Implement org credit line debt and bank repayment flow #2

Merged
J.Schmidt92 merged 14 commits from development into master 2026-04-02 16:50:38 -05:00
37 changed files with 2396 additions and 1658 deletions
Showing only changes of commit 445a114c1c - Show all commits

View File

@ -37,10 +37,6 @@ GVAR(BankRepositoryBaseClass) = compileFinal createHashMapFromArray [
["markLoaded", compileFinal {
if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; };
true
}],
["save", compileFinal {
[SRPC(bank,requestSaveBank), [getPlayerUID player]] call CFUNC(serverEvent);
_self set ["lastSave", time];
}]
];

View File

@ -37,10 +37,6 @@ GVAR(OrgRepositoryBaseClass) = compileFinal createHashMapFromArray [
["markLoaded", compileFinal {
if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; };
true
}],
["save", compileFinal {
[SRPC(org,requestSaveOrg), [getPlayerUID player]] call CFUNC(serverEvent);
_self set ["lastSave", time];
}]
];

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,45 @@
target.splice(0, target.length, ...cloneValue(source));
}
function normalizeRecord(value) {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value;
}
if (Array.isArray(value)) {
const isEntryArray = value.every(
(entry) =>
Array.isArray(entry) &&
entry.length >= 2 &&
typeof entry[0] === "string",
);
if (isEntryArray) {
return Object.fromEntries(value);
}
}
if (typeof value === "string" && value.trim() !== "") {
try {
return normalizeRecord(JSON.parse(value));
} catch (_error) {
return value;
}
}
return value;
}
function normalizeCollection(value) {
const source = Array.isArray(value)
? value
: value && typeof value === "object"
? Object.values(value)
: [];
return source.map(normalizeRecord).filter(Boolean);
}
OrgPortal.data = {
portalData: {
org: Object.assign(
@ -80,25 +119,28 @@
this.portalData.reputation = payload.portalData.reputation || 0;
replaceArray(
this.portalData.creditLines,
payload.portalData.creditLines || [],
normalizeCollection(payload.portalData.creditLines),
);
replaceArray(
this.portalData.members,
payload.portalData.members || [],
normalizeCollection(payload.portalData.members),
);
replaceArray(
this.portalData.fleet,
normalizeCollection(payload.portalData.fleet),
);
replaceArray(this.portalData.fleet, payload.portalData.fleet || []);
replaceArray(
this.portalData.assets,
payload.portalData.assets || [],
normalizeCollection(payload.portalData.assets),
);
replaceArray(
this.portalData.activity,
payload.portalData.activity || [],
normalizeCollection(payload.portalData.activity),
);
replaceArray(
this.portalData.roadmap,
payload.portalData.roadmap || [],
normalizeCollection(payload.portalData.roadmap),
);
replaceObject(this.session, payload.session || {});

View File

@ -3,6 +3,45 @@
const { createSignal } = window.RegistryApp.runtime;
const { portalData } = OrgPortal.data;
function normalizeRecord(value) {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value;
}
if (Array.isArray(value)) {
const isEntryArray = value.every(
(entry) =>
Array.isArray(entry) &&
entry.length >= 2 &&
typeof entry[0] === "string",
);
if (isEntryArray) {
return Object.fromEntries(value);
}
}
if (typeof value === "string" && value.trim() !== "") {
try {
return normalizeRecord(JSON.parse(value));
} catch (_error) {
return value;
}
}
return value;
}
function normalizeCollection(value) {
const source = Array.isArray(value)
? value
: value && typeof value === "object"
? Object.values(value)
: [];
return source.map(normalizeRecord).filter(Boolean);
}
class OrgPortalStore {
constructor() {
[this.getFunds, this.setFunds] = createSignal(portalData.funds);
@ -37,11 +76,13 @@
this.setFunds(nextPortalData.funds || 0);
this.setReputation(nextPortalData.reputation || 0);
this.setMembers([...(nextPortalData.members || [])]);
this.setCreditLines([...(nextPortalData.creditLines || [])]);
this.setFleet([...(nextPortalData.fleet || [])]);
this.setAssets([...(nextPortalData.assets || [])]);
this.setActivity([...(nextPortalData.activity || [])]);
this.setMembers([...normalizeCollection(nextPortalData.members)]);
this.setCreditLines([
...normalizeCollection(nextPortalData.creditLines),
]);
this.setFleet([...normalizeCollection(nextPortalData.fleet)]);
this.setAssets([...normalizeCollection(nextPortalData.assets)]);
this.setActivity([...normalizeCollection(nextPortalData.activity)]);
}
}

View File

@ -20,15 +20,6 @@ PREP_RECOMPILE_END;
GVAR(BankStore) call ["hydrateSession", [_uid, _mode, _resetAuthorization]];
}] call CFUNC(addEventHandler);
[QGVAR(requestSaveBank), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" };
private _finalData = GVAR(BankStore) call ["save", [_uid]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalData]];
}] call CFUNC(addEventHandler);
[QGVAR(requestDeposit), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];

View File

@ -13,40 +13,6 @@ PREP_RECOMPILE_END;
GVAR(GarageStore) call ["init", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestGetGarage), {
params [["_uid", "", [""]], ["_field", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" };
private _finalData = GVAR(GarageStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncGarage), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSetGarage), {
params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID or Key!" };
private _hashMap = GVAR(GarageStore) call ["set", [_uid, _key, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncGarage), [_hashMap], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestMSetGarage), {
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid field pairs!" };
private _hashMap = GVAR(GarageStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncGarage), [_hashMap], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSaveGarage), {
params [["_uid", "", [""]]];
@ -58,13 +24,6 @@ PREP_RECOMPILE_END;
[CRPC(garage,responseSyncGarage), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveGarage), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" };
GVAR(GarageStore) call ["remove", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestStoreVehicle), {
params [
["_uid", "", [""]],
@ -144,40 +103,6 @@ PREP_RECOMPILE_END;
GVAR(VGarageStore) call ["init", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestGetVG), {
params [["_uid", "", [""]], ["_field", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" };
private _finalData = GVAR(VGarageStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncVG), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSetVG), {
params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID or Key!" };
private _hashMap = GVAR(VGarageStore) call ["set", [_uid, _key, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncVG), [_hashMap], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestMSetVG), {
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid field pairs!" };
private _hashMap = GVAR(VGarageStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncVG), [_hashMap], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSaveVG), {
params [["_uid", "", [""]]];
@ -189,9 +114,3 @@ PREP_RECOMPILE_END;
[CRPC(garage,responseSyncVG), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveVG), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" };
GVAR(VGarageStore) call ["remove", [_uid]];
}] call CFUNC(addEventHandler);

View File

@ -67,93 +67,12 @@ GVAR(GarageBaseStore) = compileFinal createHashMapFromArray [
[CRPC(garage,responseInitGarage), [_garage], _player] call CFUNC(targetEvent);
_garage
}],
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
private _garage = _self call ["loadHotGarage", [_uid, false]];
if (_garage isEqualTo createHashMap) then {
_garage = _self call ["loadHotGarage", [_uid, true]];
};
if (_field isEqualTo "") exitWith { _garage };
_garage getOrDefault [_field, createHashMap]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
["_data", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_data isEqualType createHashMap) exitWith { createHashMap };
private _garage = _self call ["callHotGarage", ["garage:hot:override", [_uid, toJSON _data]]];
if (_save && { _garage isNotEqualTo createHashMap }) then {
private _savedGarage = _self call ["callHotGarage", ["garage:hot:save", [_uid]]];
if (_savedGarage isNotEqualTo createHashMap) then {
_garage = _savedGarage;
} else {
_garage = createHashMap;
};
};
_garage
}],
["set", compileFinal {
params [
["_uid", "", [""]],
["_field", "", [""]],
["_value", nil, [0, "", [], false, createHashMap, objNull, grpNull]],
["_sync", false, [false]]
];
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _garage = _self call ["get", [_uid, ""]];
if !(_garage isEqualType createHashMap) exitWith { createHashMap };
_garage set [_field, _value];
private _updatedGarage = _self call ["override", [_uid, _garage, _sync]];
if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap };
if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedGarage getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_uid", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _garage = _self call ["get", [_uid, ""]];
if !(_garage isEqualType createHashMap) exitWith { createHashMap };
{ _garage set [_x, _y]; } forEach _fieldValuePairs;
private _updatedGarage = _self call ["override", [_uid, _garage, _sync]];
if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap };
if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
_self call ["callHotGarage", ["garage:hot:save", [_uid]]]
}],
["remove", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
["garage:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["storeVehicle", compileFinal {
params [
["_uid", "", [""]],

View File

@ -84,159 +84,11 @@ GVAR(VGBaseStore) = compileFinal createHashMapFromArray [
[CRPC(garage,responseInitVG), [_garage], _player] call CFUNC(targetEvent);
_garage
}],
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
private _garage = _self call ["loadHotVGarage", [_uid, false]];
if (_garage isEqualTo createHashMap) then {
_garage = _self call ["loadHotVGarage", [_uid, true]];
};
if (_field isEqualTo "") exitWith { _garage };
_garage getOrDefault [_field, []]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
["_data", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_data isEqualType createHashMap) exitWith { createHashMap };
private _garage = _self call ["callHotVGarage", ["owned:garage:hot:override", [_uid, toJSON _data]]];
if (_save && { _garage isNotEqualTo createHashMap }) then {
private _savedGarage = _self call ["callHotVGarage", ["owned:garage:hot:save", [_uid]]];
if (_savedGarage isNotEqualTo createHashMap) then {
_garage = _savedGarage;
} else {
_garage = createHashMap;
};
};
_garage
}],
["set", compileFinal {
params [
["_uid", "", [""]],
["_field", "", [""]],
["_value", nil, [[], "", 0, false, createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _garage = _self call ["loadHotVGarage", [_uid, false]];
if !(_garage isEqualType createHashMap) exitWith { createHashMap };
_garage set [_field, _value];
private _updatedGarage = _self call ["override", [_uid, _garage, _sync]];
if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap };
if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedGarage getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_uid", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _garage = _self call ["loadHotVGarage", [_uid, false]];
if !(_garage isEqualType createHashMap) exitWith { createHashMap };
{ _garage set [_x, _y]; } forEach _fieldValuePairs;
private _updatedGarage = _self call ["override", [_uid, _garage, _sync]];
if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap };
if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
_self call ["callHotVGarage", ["owned:garage:hot:save", [_uid]]]
}],
["remove", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
["owned:garage:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["grantVehicles", compileFinal {
params [["_uid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]]];
private _result = createHashMapFromArray [
["success", false],
["message", "Virtual garage grant failed."],
["patch", createHashMap],
["granted", []],
["garage", createHashMap]
];
private _garage = +(_self call ["loadHotVGarage", [_uid, false]]);
if (_garage isEqualTo createHashMap) then {
_garage = GVAR(VGarageModel) call ["defaults", []];
};
private _patch = createHashMap;
private _granted = [];
private _categoriesToSync = [];
{
private _className = _x getOrDefault ["classname", ""];
private _category = toLowerANSI (_x getOrDefault ["category", ""]);
if (_className isEqualTo "") exitWith {
_result set ["message", "Vehicle checkout entry was missing a classname."];
};
if !(_category in ["cars", "armor", "helis", "planes", "naval", "other"]) exitWith {
_result set ["message", format ["Vehicle category '%1' is unsupported.", _category]];
};
private _categoryUnlocks = +(_garage getOrDefault [_category, []]);
_categoryUnlocks pushBackUnique _className;
_garage set [_category, _categoryUnlocks];
_categoriesToSync pushBackUnique _category;
_granted pushBack (createHashMapFromArray [
["classname", _className],
["category", _category]
]);
} forEach _vehicles;
{
private _category = _x;
_patch set [_category, _garage getOrDefault [_category, []]];
} forEach _categoriesToSync;
if (_commit) then {
private _savedGarage = _self call ["override", [_uid, _garage, false]];
if !(_savedGarage isEqualType createHashMap) exitWith {
_result set ["message", "Virtual garage cache update returned invalid data."];
_result
};
if (_savedGarage isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update virtual garage cache."];
_result
};
_garage = _savedGarage;
};
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result set ["granted", _granted];
_result set ["garage", _garage];
_result
}]
];

View File

@ -13,40 +13,6 @@ PREP_RECOMPILE_END;
GVAR(LockerStore) call ["init", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestGetLocker), {
params [["_uid", "", [""]], ["_field", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" };
private _finalData = GVAR(LockerStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSetLocker), {
params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID or Field!" };
private _hashMap = GVAR(LockerStore) call ["set", [_uid, _field, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncLocker), [_hashMap], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestMSetLocker), {
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid field pairs!" };
private _hashMap = GVAR(LockerStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncLocker), [_hashMap], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSaveLocker), {
params [["_uid", "", [""]]];
@ -68,13 +34,6 @@ PREP_RECOMPILE_END;
[CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveLocker), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" };
GVAR(LockerStore) call ["remove", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestInitVA), {
params [["_uid", "", [""]]];
@ -82,40 +41,6 @@ PREP_RECOMPILE_END;
GVAR(VAStore) call ["init", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestGetVA), {
params [["_uid", "", [""]], ["_field", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" };
private _finalData = GVAR(VAStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSetVA), {
params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID or Field!" };
private _hashMap = GVAR(VAStore) call ["set", [_uid, _field, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestMSetVA), {
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid field pairs!" };
private _hashMap = GVAR(VAStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSaveVA), {
params [["_uid", "", [""]]];
@ -127,9 +52,3 @@ PREP_RECOMPILE_END;
[CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveVA), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" };
GVAR(VAStore) call ["remove", [_uid]];
}] call CFUNC(addEventHandler);

View File

@ -67,17 +67,6 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [
[CRPC(locker,responseInitLocker), [_locker], _player] call CFUNC(targetEvent);
_locker
}],
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
private _locker = _self call ["loadHotLocker", [_uid, false]];
if (_locker isEqualTo createHashMap) then {
_locker = _self call ["loadHotLocker", [_uid, true]];
};
if (_field isEqualTo "") exitWith { _locker };
_locker getOrDefault [_field, createHashMap]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
@ -100,133 +89,11 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [
_locker
}],
["set", compileFinal {
params [
["_uid", "", [""]],
["_field", "", [""]],
["_value", nil, [0, "", [], false, createHashMap, objNull, grpNull]],
["_sync", false, [false]]
];
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _locker = _self call ["get", [_uid, ""]];
if !(_locker isEqualType createHashMap) exitWith { createHashMap };
_locker set [_field, _value];
private _updatedLocker = _self call ["override", [_uid, _locker, _sync]];
if !(_updatedLocker isEqualType createHashMap) exitWith { createHashMap };
if (_updatedLocker isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedLocker getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_uid", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _locker = _self call ["get", [_uid, ""]];
if !(_locker isEqualType createHashMap) exitWith { createHashMap };
{ _locker set [_x, _y]; } forEach _fieldValuePairs;
private _updatedLocker = _self call ["override", [_uid, _locker, _sync]];
if !(_updatedLocker isEqualType createHashMap) exitWith { createHashMap };
if (_updatedLocker isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
_self call ["callHotLocker", ["locker:hot:save", [_uid]]]
}],
["remove", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
["locker:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["grantItems", compileFinal {
params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]];
private _result = createHashMapFromArray [
["success", false],
["message", "Locker grant failed."],
["patch", createHashMap],
["granted", []],
["locker", createHashMap]
];
private _locker = +(_self call ["get", [_uid, ""]]);
private _patch = createHashMap;
private _granted = [];
{
private _className = _x getOrDefault ["classname", ""];
private _category = toLowerANSI (_x getOrDefault ["category", ""]);
private _quantity = floor ((_x getOrDefault ["quantity", 0]) max 0);
private _lockerCategory = switch (_category) do {
case "item";
case "attachment": { "item" };
case "weapon": { "weapon" };
case "magazine": { "magazine" };
case "backpack": { "backpack" };
default { "" };
};
if (_className isEqualTo "" || { _lockerCategory isEqualTo "" } || { _quantity <= 0 }) then {
["WARN", format ["Skipping invalid locker grant entry: %1 (category: %2)", _className, _category]] call EFUNC(common,log);
} else {
private _entry = +(_locker getOrDefault [_className, createHashMap]);
private _amount = _entry getOrDefault ["amount", 0];
private _updatedEntry = createHashMapFromArray [
["amount", (_amount + _quantity)],
["classname", _className],
["category", _lockerCategory]
];
_locker set [_className, _updatedEntry];
_patch set [_className, _updatedEntry];
_granted pushBack (createHashMapFromArray [
["classname", _className],
["category", _lockerCategory],
["quantity", _quantity]
]);
};
} forEach _items;
if ((count (keys _locker)) > 25) exitWith {
_result set ["message", "Locker capacity would exceed 25 unique items. Clear space before checkout."];
_result
};
if (_commit) then {
private _savedLocker = _self call ["override", [_uid, _locker, false]];
if !(_savedLocker isEqualType createHashMap) exitWith {
_result set ["message", "Locker cache update returned invalid data."];
_result
};
if (_savedLocker isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update locker cache."];
_result
};
_locker = _savedLocker;
};
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result set ["granted", _granted];
_result set ["locker", _locker];
_result
}]
];

View File

@ -82,152 +82,11 @@ GVAR(VABaseStore) = compileFinal createHashMapFromArray [
[CRPC(locker,responseInitVA), [_arsenal], _player] call CFUNC(targetEvent);
_arsenal
}],
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
private _arsenal = _self call ["loadHotVArsenal", [_uid, false]];
if (_arsenal isEqualTo createHashMap) then {
_arsenal = _self call ["loadHotVArsenal", [_uid, true]];
};
if (_field isEqualTo "") exitWith { _arsenal };
_arsenal getOrDefault [_field, []]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
["_data", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_data isEqualType createHashMap) exitWith { createHashMap };
private _arsenal = _self call ["callHotVArsenal", ["owned:locker:hot:override", [_uid, toJSON _data]]];
if (_save && { _arsenal isNotEqualTo createHashMap }) then {
private _savedArsenal = _self call ["callHotVArsenal", ["owned:locker:hot:save", [_uid]]];
if (_savedArsenal isNotEqualTo createHashMap) then {
_arsenal = _savedArsenal;
} else {
_arsenal = createHashMap;
};
};
_arsenal
}],
["set", compileFinal {
params [
["_uid", "", [""]],
["_field", "", [""]],
["_value", nil, [[], "", 0, false, createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _arsenal = _self call ["get", [_uid, ""]];
if !(_arsenal isEqualType createHashMap) exitWith { createHashMap };
_arsenal set [_field, _value];
private _updatedArsenal = _self call ["override", [_uid, _arsenal, _sync]];
if !(_updatedArsenal isEqualType createHashMap) exitWith { createHashMap };
if (_updatedArsenal isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedArsenal getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_uid", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _arsenal = _self call ["get", [_uid, ""]];
if !(_arsenal isEqualType createHashMap) exitWith { createHashMap };
{ _arsenal set [_x, _y]; } forEach _fieldValuePairs;
private _updatedArsenal = _self call ["override", [_uid, _arsenal, _sync]];
if !(_updatedArsenal isEqualType createHashMap) exitWith { createHashMap };
if (_updatedArsenal isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
_self call ["callHotVArsenal", ["owned:locker:hot:save", [_uid]]]
}],
["remove", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
["owned:locker:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["unlockItems", compileFinal {
params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]];
private _result = createHashMapFromArray [
["success", false],
["message", "VA unlock failed."],
["patch", createHashMap],
["arsenal", createHashMap]
];
private _arsenal = +(_self call ["get", [_uid, ""]]);
if (_arsenal isEqualTo createHashMap) then {
_arsenal = GVAR(VArsenalModel) call ["defaults", []];
};
private _patch = createHashMap;
private _categoriesToSync = [];
{
private _item = _x;
private _className = _item getOrDefault ["classname", ""];
private _category = toLowerANSI (_item getOrDefault ["category", ""]);
private _arsenalCategory = switch (_category) do {
case "item": { "items" };
case "weapon": { "weapons" };
case "magazine": { "magazines" };
case "backpack": { "backpacks" };
default { "items" };
};
private _categoryUnlocks = +(_arsenal getOrDefault [_arsenalCategory, []]);
_categoryUnlocks pushBackUnique _className;
_arsenal set [_arsenalCategory, _categoryUnlocks];
_categoriesToSync pushBackUnique _arsenalCategory;
} forEach _items;
{
private _category = _x;
private _categoryUnlocks = _arsenal getOrDefault [_category, []];
_patch set [_category, _categoryUnlocks];
} forEach _categoriesToSync;
if (_commit) then {
private _savedArsenal = _self call ["override", [_uid, _arsenal, false]];
if !(_savedArsenal isEqualType createHashMap) exitWith {
_result set ["message", "Virtual arsenal cache update returned invalid data."];
_result
};
if (_savedArsenal isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update virtual arsenal cache."];
_result
};
_arsenal = _savedArsenal;
};
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result set ["arsenal", _arsenal];
_result
}]
];

View File

@ -42,6 +42,7 @@ if (isNil QEGVAR(locker,LockerStore)) then { call EFUNC(locker,initLockerStore);
if (isNil QEGVAR(locker,VAStore)) then { call EFUNC(locker,initVAStore); };
// Org
if (isNil QEGVAR(org,OrgPayloadBuilder)) then { call EFUNC(org,initPayloadBuilder); };
if (isNil QEGVAR(org,OrgStore)) then { call EFUNC(org,initOrgStore); };
// Store

View File

@ -1,3 +1,2 @@
PREP(initPayloadBuilder);
PREP(initOrgStore);
PREP(memberService);
PREP(treasuryService);

View File

@ -52,38 +52,6 @@ PREP_RECOMPILE_END;
[CRPC(org,responseCreateOrg), [_result], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestGetOrg), {
params [["_uid", "", [""]], ["_field", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" };
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
private _finalData = GVAR(OrgStore) call ["get", [_key, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(org,responseSyncOrg), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSetOrg), {
params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID or Field!" };
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
GVAR(OrgStore) call ["set", [_key, _field, _value, _sync]];
}] call CFUNC(addEventHandler);
[QGVAR(requestMSetOrg), {
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid field pairs!" };
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
GVAR(OrgStore) call ["mset", [_key, _fieldValuePairs, _sync]];
}] call CFUNC(addEventHandler);
[QGVAR(requestAssignCreditLine), {
params [
["_uid", "", [""]],
@ -117,24 +85,6 @@ PREP_RECOMPILE_END;
]], _requester] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSaveOrg), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" };
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
GVAR(OrgStore) call ["saveById", [_key]];
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveOrg), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" };
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
GVAR(OrgStore) call ["delete", [_key]];
}] call CFUNC(addEventHandler);
[QGVAR(requestLeaveOrg), {
params [["_uid", "", [""]]];

View File

@ -21,9 +21,6 @@
* call forge_server_org_fnc_initOrgStore
*/
if (isNil QGVAR(OrgMembershipService)) then { call FUNC(memberService); };
if (isNil QGVAR(OrgTreasuryService)) then { call FUNC(treasuryService); };
#pragma hemtt ignore_variables ["_self"]
GVAR(OrgModel) = compileFinal createHashMapObject [[
["#type", "OrgModel"],
@ -190,6 +187,31 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_self call ["syncHotOrg", [_data]]
}],
["callHotOrgEnvelope", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
if (_function isEqualTo "") exitWith { createHashMap };
[_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith { createHashMap };
if !(_result isEqualType "") exitWith { createHashMap };
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["Org extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
createHashMap
};
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith { createHashMap };
if ("org" in _data) then {
private _syncedOrg = _self call ["syncHotOrg", [_data getOrDefault ["org", createHashMap]]];
if (_syncedOrg isNotEqualTo createHashMap) then {
_data set ["org", _syncedOrg];
};
};
_data
}],
["syncHotOrg", compileFinal {
params [["_org", createHashMap, [createHashMap]]];
@ -211,10 +233,48 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_orgID isEqualTo "") then { _orgID = "default"; };
_orgID
}],
["loadForUid", compileFinal {
params [["_uid", "", [""]]];
private _orgID = _self call ["resolveOrgIdForUid", [_uid]];
_self call ["loadById", [_orgID]]
["resolveActorName", compileFinal {
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
private _memberName = _actor getOrDefault ["name", ""];
if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then {
_memberName = name _player;
};
if (_memberName isEqualTo "") then { _memberName = "Unknown"; };
_memberName
}],
["applyActorOrganization", compileFinal {
params [["_uid", "", [""]], ["_orgID", "", [""]], ["_actor", createHashMap, [createHashMap]]];
if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith { createHashMap };
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, false]];
private _updatedActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]];
if (
!(_updatedActor isEqualType createHashMap)
|| { _updatedActor isEqualTo createHashMap }
|| { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID }
) then {
private _forcedActor = +_actor;
if !(_forcedActor isEqualType createHashMap) then {
_forcedActor = EGVAR(actor,ActorModel) call ["defaults", []];
_forcedActor set ["uid", _uid];
};
_forcedActor set ["organization", _orgID];
_updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]];
if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then {
_actorPatch = createHashMapFromArray [["organization", _orgID]];
};
};
if (
!(_updatedActor isEqualType createHashMap)
|| { _updatedActor isEqualTo createHashMap }
|| { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID }
) exitWith { createHashMap };
_actorPatch
}],
["loadHotOrg", compileFinal {
params [["_orgID", "", [""]], ["_initialize", false, [false]]];
@ -235,80 +295,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_field isEqualTo "") exitWith { _org };
_org getOrDefault [_field, createHashMap]
}],
["override", compileFinal {
params [
["_orgID", "", [""]],
["_org", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_orgID isEqualTo "") exitWith { createHashMap };
if !(_org isEqualType createHashMap) exitWith { createHashMap };
private _normalizedOrg = +_org;
_normalizedOrg set ["id", _normalizedOrg getOrDefault ["id", _orgID]];
private _result = _self call ["callHotOrg", ["org:hot:override", [_orgID, toJSON _normalizedOrg]]];
if (_save && { _result isNotEqualTo createHashMap }) then {
private _savedOrg = _self call ["callHotOrg", ["org:hot:save", [_orgID]]];
if (_savedOrg isNotEqualTo createHashMap) then {
_result = _savedOrg;
} else {
_result = createHashMap;
};
};
_result
}],
["set", compileFinal {
params [
["_orgID", "", [""]],
["_field", "", [""]],
["_value", nil, [[], "", 0, false, createHashMap]],
["_sync", false, [false]]
];
if (_orgID isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _org = _self call ["get", [_orgID, ""]];
if !(_org isEqualType createHashMap) exitWith { createHashMap };
_org set [_field, _value];
private _updatedOrg = _self call ["override", [_orgID, _org, _sync]];
if !(_updatedOrg isEqualType createHashMap) exitWith { createHashMap };
if (_updatedOrg isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedOrg getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_orgID", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_orgID isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _org = _self call ["get", [_orgID, ""]];
if !(_org isEqualType createHashMap) exitWith { createHashMap };
{ _org set [_x, _y]; } forEach _fieldValuePairs;
private _updatedOrg = _self call ["override", [_orgID, _org, _sync]];
if !(_updatedOrg isEqualType createHashMap) exitWith { createHashMap };
if (_updatedOrg isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["verifyMember", compileFinal {
GVAR(OrgMembershipService) call ["verifyMember", _this]
}],
["addMember", compileFinal {
GVAR(OrgMembershipService) call ["addMember", _this]
}],
["removeMember", compileFinal {
GVAR(OrgMembershipService) call ["removeMember", _this]
}],
["delete", compileFinal {
params [["_orgID", "", [""]]];
@ -332,158 +318,213 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result set ["success", true];
_result
}],
["restoreDefaultMembership", compileFinal {
GVAR(OrgMembershipService) call ["restoreDefaultMembership", _this]
["ensureMember", compileFinal {
params [["_orgID", "", [""]], ["_uid", "", [""]], ["_memberName", "", [""]]];
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
private _context = createHashMapFromArray [
["orgId", _orgID],
["memberUid", _uid],
["memberName", _memberName]
];
_self call ["callHotOrg", ["org:hot:ensure_member", [toJSON _context]]]
}],
["leave", compileFinal {
GVAR(OrgMembershipService) call ["leave", _this]
params [["_uid", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["actorPatch", createHashMap],
["notification", []]
];
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required."];
_result
};
private _player = [_uid] call EFUNC(common,getPlayer);
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", "default"];
private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]];
private _context = createHashMapFromArray [
["requesterUid", _uid],
["requesterName", _memberName],
["orgId", _orgID]
];
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:leave", [toJSON _context]]];
if (_envelope isEqualTo createHashMap) exitWith {
_result set ["message", "Unable to leave the organization."];
_result
};
private _actorOrg = _envelope getOrDefault ["actorOrganization", "default"];
private _actorPatch = _self call ["applyActorOrganization", [_uid, _actorOrg, _actor]];
if (_actorPatch isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to restore default organization membership."];
_result
};
_result set ["success", true];
_result set ["message", _envelope getOrDefault ["message", "You returned to the default organization."]];
_result set ["actorPatch", _actorPatch];
_result set ["notification", ["info", "Organization Left", _result get "message", 6000]];
_result
}],
["disband", compileFinal {
GVAR(OrgMembershipService) call ["disband", _this]
params [["_uid", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["members", []]
];
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required."];
_result
};
private _player = [_uid] call EFUNC(common,getPlayer);
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", "default"];
private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]];
private _context = createHashMapFromArray [
["requesterUid", _uid],
["requesterName", _memberName],
["orgId", _orgID]
];
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:disband", [toJSON _context]]];
if (_envelope isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to disband organization."];
_result
};
private _memberResults = [];
{
private _memberUid = _x getOrDefault ["uid", ""];
if (_memberUid isEqualTo "") then { continue; };
private _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap];
private _actorPatch = _self call ["applyActorOrganization", [_memberUid, _x getOrDefault ["actorOrganization", "default"], _memberActor]];
if (_actorPatch isEqualTo createHashMap) then {
["WARNING", format ["Failed to restore actor organization for %1 after org disband.", _memberUid]] call EFUNC(common,log);
};
private _responseMessage = _x getOrDefault ["message", _envelope getOrDefault ["message", "Organization disbanded."]];
private _notificationParams = [
["warning", "Organization Disbanded", _responseMessage, 6000],
["success", "Organization Disbanded", _responseMessage, 6000]
] select (_x getOrDefault ["requester", false]);
_memberResults pushBack (createHashMapFromArray [
["uid", _memberUid],
["requester", _x getOrDefault ["requester", false]],
["message", _responseMessage],
["notification", _notificationParams],
["actorPatch", _actorPatch]
]);
} forEach (_envelope getOrDefault ["members", []]);
_result set ["success", true];
_result set ["message", _envelope getOrDefault ["message", "Organization disbanded."]];
_result set ["members", _memberResults];
_result
}],
["assignCreditLine", compileFinal {
GVAR(OrgTreasuryService) call ["assignCreditLine", _this]
params [["_requesterUid", "", [""]], ["_memberUid", "", [""]], ["_memberName", "", [""]], ["_amount", 0, [0]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["patch", createHashMap],
["memberUids", []]
];
if (_requesterUid isEqualTo "" || { _memberUid isEqualTo "" } || { _amount <= 0 }) exitWith {
_result set ["message", "A valid requester, member, and credit amount are required."];
_result
};
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
private _orgID = _requesterActor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
private _requesterIsDefaultOrgCeo = (
_requesterPlayer isNotEqualTo objNull
&& { _orgID isEqualTo "default" }
&& { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" }
);
private _context = createHashMapFromArray [
["requesterUid", _requesterUid],
["orgId", _orgID],
["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo],
["memberUid", _memberUid],
["memberName", _memberName],
["amount", _amount]
];
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:assign_credit_line", [toJSON _context]]];
if (_envelope isEqualTo createHashMap) exitWith {
_result set ["message", "Unable to assign credit line."];
_result
};
_result set ["success", true];
_result set ["message", _envelope getOrDefault ["message", "Credit line assigned."]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
_result
}],
["buildPortalPayload", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) exitWith { createHashMap };
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _org = _self call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) then {
_org = _self call ["init", [_uid]];
};
if (_org isEqualTo createHashMap) exitWith { createHashMap };
// Ensure the requesting player's membership is present in the cached roster
// before shaping the portal payload. This prevents stale org caches from
// omitting the current member while still resolving owner metadata.
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
private _name = _org getOrDefault ["name", ""];
private _id = _org getOrDefault ["id", _orgID];
private _ownerUid = _org getOrDefault ["owner", ""];
private _funds = _org getOrDefault ["funds", 0];
private _reputation = _org getOrDefault ["reputation", 0];
private _creditLinesRaw = _org getOrDefault ["credit_lines", createHashMap];
private _assetsRaw = _org getOrDefault ["assets", createHashMap];
private _fleetRaw = _org getOrDefault ["fleet", createHashMap];
private _membersRaw = _org getOrDefault ["members", createHashMap];
private _isDefaultOrg = (_org getOrDefault ["default", false])
|| { toLower _id isEqualTo "default" }
|| { toLower _ownerUid isEqualTo "server" };
private _playerName = name _player;
private _playerVar = vehicleVarName _player;
private _sessionRole = "Member";
private _sessionIsCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" };
private _ownerName = ["", "Server"] select (toLower _ownerUid isEqualTo "server");
private _membersList = [];
{
private _memberData = _y;
private _memberName = _memberData getOrDefault ["name", "Unknown"];
private _memberUid = _memberData getOrDefault ["uid", ""];
if (_memberUid isEqualTo _ownerUid && { _ownerName isEqualTo "" }) then { _ownerName = _memberName; };
if (_memberUid isEqualTo _uid) then { _sessionRole = "Member"; };
_membersList pushBack (createHashMapFromArray [
["uid", _memberUid],
["name", _memberName]
]);
} forEach _membersRaw;
if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _uid }) then { _ownerName = _playerName; };
if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; };
if (_ownerUid isEqualTo _uid) then { _sessionRole = "Leader"; };
private _assetsList = [];
{
private _category = _x;
{
private _assetData = _y;
private _className = _assetData getOrDefault ["classname", ""];
private _displayName = _className;
{
private _cfg = _x >> _className;
if (isClass _cfg) exitWith {
private _resolvedName = getText (_cfg >> "displayName");
if (_resolvedName isNotEqualTo "") then { _displayName = _resolvedName; };
};
} forEach [
configFile >> "CfgWeapons",
configFile >> "CfgMagazines",
configFile >> "CfgVehicles",
configFile >> "CfgGlasses"
];
_assetsList pushBack (createHashMapFromArray [
["name", _displayName],
["type", _assetData getOrDefault ["type", _category]],
["quantity", str (_assetData getOrDefault ["quantity", 0])]
]);
} forEach _y;
} forEach _assetsRaw;
private _fleetList = [];
{
private _vehicleData = _y;
_fleetList pushBack (createHashMapFromArray [
["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]],
["type", _vehicleData getOrDefault ["type", "other"]],
["status", _vehicleData getOrDefault ["status", "Unknown"]],
["damage", _vehicleData getOrDefault ["damage", "0%"]]
]);
} forEach _fleetRaw;
private _creditLinesList = [];
{
private _creditLineData = _y;
_creditLinesList pushBack (createHashMapFromArray [
["uid", _creditLineData getOrDefault ["uid", _x]],
["member", _creditLineData getOrDefault ["name", "Unknown Member"]],
["amount", _creditLineData getOrDefault ["amount", 0]]
]);
} forEach _creditLinesRaw;
createHashMapFromArray [
["session", createHashMapFromArray [
["actorName", _playerName],
["actorUid", _uid],
["role", _sessionRole],
["ceo", _sessionIsCeo]
]],
["portalData", createHashMapFromArray [
["org", createHashMapFromArray [
["name", _name],
["tag", _id],
["owner", _ownerName],
["ownerUid", _ownerUid],
["isDefault", _isDefaultOrg]
]],
["funds", _funds],
["reputation", _reputation],
["creditLines", _creditLinesList],
["members", _membersList],
["fleet", _fleetList],
["assets", _assetsList],
["activity", []]
]]
]
}],
["buildChargeResult", compileFinal {
GVAR(OrgTreasuryService) call ["buildChargeResult", _this]
GVAR(OrgPayloadBuilder) call ["buildPortalPayload", [_uid]]
}],
["chargeCheckout", compileFinal {
GVAR(OrgTreasuryService) call ["chargeCheckout", _this]
params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]];
private _result = createHashMapFromArray [
["success", false],
["message", "Unable to process organization payment."],
["patch", createHashMap],
["memberUids", []]
];
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
private _orgID = _requesterActor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _requesterIsDefaultOrgCeo = (
_requesterPlayer isNotEqualTo objNull
&& { _orgID isEqualTo "default" }
&& { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" }
);
private _context = createHashMapFromArray [
["requesterUid", _requesterUid],
["orgId", _orgID],
["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo],
["source", _source],
["amount", _amount],
["commit", _commit]
];
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:charge_checkout", [toJSON _context]]];
if (_envelope isEqualTo createHashMap) exitWith { _result };
_result set ["success", true];
_result set ["message", _envelope getOrDefault ["message", ""]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
_result
}],
["saveById", compileFinal {
params [["_orgID", "", [""]]];
@ -515,42 +556,29 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; };
private _org = _self call ["loadById", [_resolvedOrgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Organization data is unavailable for asset updates."];
_result
private _context = createHashMapFromArray [
["requesterUid", _requesterUid],
["orgId", _resolvedOrgID],
["commit", _commit]
];
private _assetSeeds = _assets apply {
createHashMapFromArray [
["classname", _x getOrDefault ["classname", ""]],
["category", toLowerANSI (_x getOrDefault ["category", "items"])],
["quantity", floor ((_x getOrDefault ["quantity", 0]) max 0)]
]
};
private _assetMap = +(_org getOrDefault ["assets", createHashMap]);
{
private _className = _x getOrDefault ["classname", ""];
private _category = toLowerANSI (_x getOrDefault ["category", "items"]);
private _quantity = floor ((_x getOrDefault ["quantity", 0]) max 0);
if (_className isEqualTo "" || { _quantity <= 0 }) then { continue; };
private _categoryMap = +(_assetMap getOrDefault [_category, createHashMap]);
private _assetEntry = +(_categoryMap getOrDefault [_className, createHashMap]);
private _existingQuantity = _assetEntry getOrDefault ["quantity", 0];
_categoryMap set [_className, createHashMapFromArray [
["classname", _className],
["type", _category],
["quantity", (_existingQuantity + _quantity)]
]];
_assetMap set [_category, _categoryMap];
} forEach _assets;
private _patch = _self call ["mset", [_resolvedOrgID, createHashMapFromArray [["assets", _assetMap]], false]];
if (_patch isEqualTo createHashMap) exitWith {
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:add_assets", [toJSON _context, toJSON _assetSeeds]]];
if (_envelope isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update organization asset cache."];
_result
};
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result set ["memberUids", GVAR(OrgTreasuryService) call ["resolveOrgMemberUids", [_org, _requesterUid]]];
_result set ["message", _envelope getOrDefault ["message", ""]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
_result
}],
["addFleetVehicles", compileFinal {
@ -576,52 +604,28 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; };
private _org = _self call ["loadById", [_resolvedOrgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Organization data is unavailable for fleet updates."];
_result
private _context = createHashMapFromArray [
["requesterUid", _requesterUid],
["orgId", _resolvedOrgID],
["commit", _commit]
];
private _fleetSeeds = _vehicles apply {
createHashMapFromArray [
["classname", _x getOrDefault ["classname", ""]],
["category", toLowerANSI (_x getOrDefault ["category", "other"])]
]
};
private _fleet = +(_org getOrDefault ["fleet", createHashMap]);
private _fleetIndex = count (keys _fleet);
{
private _className = _x getOrDefault ["classname", ""];
private _category = toLowerANSI (_x getOrDefault ["category", "other"]);
if (_className isEqualTo "") exitWith {
_result set ["message", "Vehicle fleet entry was missing a classname."];
};
private _fleetKey = format ["%1_%2", _className, _fleetIndex];
while { _fleetKey in (keys _fleet) } do {
_fleetIndex = _fleetIndex + 1;
_fleetKey = format ["%1_%2", _className, _fleetIndex];
};
private _displayName = getText (configFile >> "CfgVehicles" >> _className >> "displayName");
if (_displayName isEqualTo "") then { _displayName = _className; };
_fleet set [_fleetKey, createHashMapFromArray [
["classname", _className],
["name", _displayName],
["type", _category],
["status", "Ready"],
["damage", "0%"]
]];
_fleetIndex = _fleetIndex + 1;
} forEach _vehicles;
private _patch = _self call ["mset", [_resolvedOrgID, createHashMapFromArray [["fleet", _fleet]], false]];
if (_patch isEqualTo createHashMap) exitWith {
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:add_fleet", [toJSON _context, toJSON _fleetSeeds]]];
if (_envelope isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update organization fleet cache."];
_result
};
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result set ["memberUids", GVAR(OrgTreasuryService) call ["resolveOrgMemberUids", [_org, _requesterUid]]];
_result set ["message", _envelope getOrDefault ["message", ""]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
_result
}],
["loadById", compileFinal {
@ -646,13 +650,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result
};
private _player = [_uid] call EFUNC(common,getPlayer);
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _existingOrgID = _actor getOrDefault ["organization", ""];
if (_existingOrgID isNotEqualTo "" && { toLower _existingOrgID isNotEqualTo "default" }) exitWith {
_result set ["message", "Player already belongs to an organization."];
_result
};
private _orgID = _actor getOrDefault ["phone_number", ""];
if (_orgID isEqualTo "") exitWith {
@ -660,80 +659,29 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result
};
["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"];
if (!_existsSuccess) exitWith {
_result set ["message", "Unable to verify organization ID availability."];
_result
};
if (_existsResult isEqualTo "true") exitWith {
_result set ["message", "An organization already exists for this phone number."];
_result
};
private _org = createHashMapFromArray [
["id", _orgID],
["owner", _uid],
["name", _orgName],
["funds", 0],
["reputation", 0],
["credit_lines", createHashMap],
["members", createHashMap]
private _context = createHashMapFromArray [
["requesterUid", _uid],
["requesterName", _self call ["resolveActorName", [_uid, [_uid] call EFUNC(common,getPlayer), _actor]]],
["orgId", _orgID],
["orgName", _orgName],
["existingOrgId", _existingOrgID]
];
private _json = _self call ["toJSON", [_org]];
["org:create", [_orgID, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"];
if (!_createSuccess) exitWith {
_result set ["message", format ["Failed to create organization: %1", _createResult]];
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:register", [toJSON _context]]];
if (_envelope isEqualTo createHashMap) exitWith {
_result set ["message", "Organization registration failed."];
_result
};
if (_createResult isNotEqualTo "") then {
_org = _self call ["toHashMap", [_createResult]];
};
_org set ["members", createHashMap];
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
if (toLower _existingOrgID isEqualTo "default") then {
private _defaultOrg = _self call ["removeMember", ["default", _uid]];
if (_defaultOrg isEqualTo createHashMap) then {
["WARNING", format ["Failed to remove %1 from default org members after creating org %2.", _uid, _orgID]] call EFUNC(common,log);
};
};
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, false]];
private _updatedActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]];
if (
!(_updatedActor isEqualType createHashMap)
|| { _updatedActor isEqualTo createHashMap }
|| { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID }
) then {
private _forcedActor = +_actor;
if !(_forcedActor isEqualType createHashMap) then {
_forcedActor = EGVAR(actor,ActorModel) call ["defaults", []];
_forcedActor set ["uid", _uid];
};
_forcedActor set ["organization", _orgID];
_updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]];
if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then {
_actorPatch = createHashMapFromArray [["organization", _orgID]];
};
};
if (
!(_updatedActor isEqualType createHashMap)
|| { _updatedActor isEqualTo createHashMap }
|| { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID }
) exitWith {
private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]];
if (_actorPatch isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to assign the player to the new organization."];
_result
};
_org = _self call ["override", [_orgID, _org, false]];
_result set ["success", true];
_result set ["org", _org];
_result set ["message", _envelope getOrDefault ["message", ""]];
_result set ["org", _envelope getOrDefault ["org", createHashMap]];
_result set ["actorPatch", _actorPatch];
_result
}],
@ -754,9 +702,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_orgID = "default";
};
private _finalOwner = _finalOrg getOrDefault ["owner", ""];
if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then {
_finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]];
private _verifiedOrg = _self call ["ensureMember", [_orgID, _uid, _self call ["resolveActorName", [_uid, _player, _actor]]]];
if (_verifiedOrg isNotEqualTo createHashMap) then {
_finalOrg = _verifiedOrg;
};
[CRPC(org,responseInitOrg), [_finalOrg], _player] call CFUNC(targetEvent);

View File

@ -0,0 +1,213 @@
#include "..\script_component.hpp"
/*
* File: fnc_initPayloadBuilder.sqf
* Author: IDSolutions
* Date: 2026-04-02
* Public: No
*
* Description:
* Initializes the org payload builder for portal/read-model shaping.
* Keeps hydrate construction out of OrgStore so the store can focus on
* extension-backed org operations and actor coordination.
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(OrgPayloadBuilder) = createHashMapObject [[
["#type", "OrgPayloadBuilder"],
["resolveOrgForUid", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _orgID = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) then {
_org = GVAR(OrgStore) call ["init", [_uid]];
};
_org
}],
["resolveOwnerName", compileFinal {
params [["_ownerUid", "", [""]], ["_uid", "", [""]], ["_playerName", "", [""]], ["_membersRaw", createHashMap, [createHashMap]]];
private _ownerName = ["", "Server"] select (toLowerANSI _ownerUid isEqualTo "server");
{
private _memberData = _y;
private _memberUid = _memberData getOrDefault ["uid", ""];
if (_memberUid isEqualTo _ownerUid && { _ownerName isEqualTo "" }) exitWith {
_ownerName = _memberData getOrDefault ["name", "Unknown"];
};
} forEach _membersRaw;
if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _uid }) then { _ownerName = _playerName; };
if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; };
if !(_ownerName isEqualType "") then { _ownerName = str _ownerName; };
_ownerName
}],
["buildMembersList", compileFinal {
params [["_membersRaw", createHashMap, [createHashMap]], ["_uid", "", [""]], ["_ownerUid", "", [""]]];
private _sessionRole = "Member";
private _membersList = [];
{
private _memberData = _y;
private _memberName = _memberData getOrDefault ["name", "Unknown"];
private _memberUid = _memberData getOrDefault ["uid", ""];
if (_memberUid isEqualTo _uid) then { _sessionRole = "Member"; };
if (_memberUid isEqualTo _ownerUid) then { _sessionRole = ["Member", "Leader"] select (_ownerUid isEqualTo _uid); };
_membersList pushBack [
["uid", _memberUid],
["name", _memberName]
];
} forEach _membersRaw;
createHashMapFromArray [
["members", _membersList],
["sessionRole", _sessionRole]
]
}],
["resolveDisplayName", compileFinal {
params [["_className", "", [""]], ["_configRoots", [], [[]]]];
if (_className isEqualTo "") exitWith { "" };
private _displayName = _className;
{
private _cfg = _x >> _className;
if (isClass _cfg) exitWith {
private _resolvedName = getText (_cfg >> "displayName");
if (_resolvedName isNotEqualTo "") then { _displayName = _resolvedName; };
};
} forEach _configRoots;
_displayName
}],
["buildAssetsList", compileFinal {
params [["_assetsRaw", createHashMap, [createHashMap]]];
private _assetsList = [];
{
private _category = _x;
{
private _assetData = _y;
private _className = _assetData getOrDefault ["classname", ""];
private _displayName = _self call ["resolveDisplayName", [_className, [
configFile >> "CfgWeapons",
configFile >> "CfgMagazines",
configFile >> "CfgVehicles",
configFile >> "CfgGlasses"
]]];
_assetsList pushBack [
["name", _displayName],
["type", _assetData getOrDefault ["type", _category]],
["quantity", str (_assetData getOrDefault ["quantity", 0])]
];
} forEach _y;
} forEach _assetsRaw;
_assetsList
}],
["buildFleetList", compileFinal {
params [["_fleetRaw", createHashMap, [createHashMap]]];
private _fleetList = [];
{
private _vehicleData = _y;
_fleetList pushBack [
["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]],
["type", _vehicleData getOrDefault ["type", "other"]],
["status", _vehicleData getOrDefault ["status", "Unknown"]],
["damage", _vehicleData getOrDefault ["damage", "0%"]]
];
} forEach _fleetRaw;
_fleetList
}],
["buildCreditLinesList", compileFinal {
params [["_creditLinesRaw", createHashMap, [createHashMap]]];
private _creditLinesList = [];
{
private _creditLineData = _y;
_creditLinesList pushBack [
["uid", _creditLineData getOrDefault ["uid", _x]],
["member", _creditLineData getOrDefault ["name", "Unknown Member"]],
["amount", _creditLineData getOrDefault ["amount", 0]]
];
} forEach _creditLinesRaw;
_creditLinesList
}],
["buildPortalPayload", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) exitWith { createHashMap };
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _org = _self call ["resolveOrgForUid", [_uid]];
if (_org isEqualTo createHashMap) exitWith { createHashMap };
private _verifiedOrg = GVAR(OrgStore) call ["ensureMember", [_orgID, _uid, GVAR(OrgStore) call ["resolveActorName", [_uid, _player, _actor]]]];
if (_verifiedOrg isNotEqualTo createHashMap) then { _org = _verifiedOrg; };
private _name = _org getOrDefault ["name", ""];
private _id = _org getOrDefault ["id", _orgID];
private _ownerUid = _org getOrDefault ["owner", ""];
private _funds = _org getOrDefault ["funds", 0];
private _reputation = _org getOrDefault ["reputation", 0];
private _creditLinesRaw = _org getOrDefault ["credit_lines", createHashMap];
private _assetsRaw = _org getOrDefault ["assets", createHashMap];
private _fleetRaw = _org getOrDefault ["fleet", createHashMap];
private _membersRaw = _org getOrDefault ["members", createHashMap];
private _isDefaultOrg = (_org getOrDefault ["default", false])
|| { toLowerANSI _id isEqualTo "default" }
|| { toLowerANSI _ownerUid isEqualTo "server" };
private _playerName = name _player;
private _playerVar = vehicleVarName _player;
private _sessionIsCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" };
private _memberShape = _self call ["buildMembersList", [_membersRaw, _uid, _ownerUid]];
private _sessionRole = _memberShape getOrDefault ["sessionRole", "Member"];
private _ownerName = _self call ["resolveOwnerName", [_ownerUid, _uid, _playerName, _membersRaw]];
if (_ownerUid isEqualTo _uid) then { _sessionRole = "Leader"; };
createHashMapFromArray [
["session", createHashMapFromArray [
["actorName", _playerName],
["actorUid", _uid],
["role", _sessionRole],
["ceo", _sessionIsCeo]
]],
["portalData", createHashMapFromArray [
["org", createHashMapFromArray [
["name", _name],
["tag", _id],
["owner", _ownerName],
["ownerUid", _ownerUid],
["isDefault", _isDefaultOrg]
]],
["funds", _funds],
["reputation", _reputation],
["creditLines", _self call ["buildCreditLinesList", [_creditLinesRaw]]],
["members", _memberShape getOrDefault ["members", []]],
["fleet", _self call ["buildFleetList", [_fleetRaw]]],
["assets", _self call ["buildAssetsList", [_assetsRaw]]],
["activity", []]
]]
]
}]
]];
GVAR(OrgPayloadBuilder)

View File

@ -1,273 +0,0 @@
#include "..\script_component.hpp"
#pragma hemtt ignore_variables ["_self"]
GVAR(OrgMembershipServiceBase) = compileFinal createHashMapFromArray [
["#type", "OrgMembershipService"],
["buildMembershipResult", compileFinal {
params [["_message", "", [""]]];
createHashMapFromArray [
["success", false],
["message", _message],
["actorPatch", createHashMap]
]
}],
["verifyMember", compileFinal {
params [["_org", createHashMap, [createHashMap]], ["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { _org };
private _members = _org getOrDefault ["members", createHashMap];
if ((_members getOrDefault [_uid, objNull]) isNotEqualTo objNull) exitWith { _org };
["org:members:add", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"];
if (!_memberSuccess) then {
["WARNING", format ["Failed to add %1 to org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log);
};
private _memberName = _actor getOrDefault ["name", ""];
if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then {
_memberName = name _player;
};
if (_memberName isEqualTo "") then {
_memberName = "Unknown";
};
private _updatedMembers = +_members;
_updatedMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]];
_org set ["members", _updatedMembers];
_org = GVAR(OrgStore) call ["override", [_orgID, _org, false]];
_org
}],
["addMember", compileFinal {
params [["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith { _org };
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
_org
}],
["removeMember", compileFinal {
params [["_orgID", "", [""]], ["_uid", "", [""]]];
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith { _org };
["org:members:remove", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"];
if (!_memberSuccess) exitWith {
["WARNING", format ["Failed to remove %1 from org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log);
createHashMap
};
private _updatedMembers = +(_org getOrDefault ["members", createHashMap]);
_updatedMembers deleteAt _uid;
_org set ["members", _updatedMembers];
_org = GVAR(OrgStore) call ["override", [_orgID, _org, false]];
_org
}],
["restoreDefaultMembership", compileFinal {
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
private _result = _self call ["buildMembershipResult", []];
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required."];
_result
};
private _resolvedPlayer = _player;
if (_resolvedPlayer isEqualTo objNull) then {
_resolvedPlayer = [_uid] call EFUNC(common,getPlayer);
};
private _resolvedActor = EGVAR(actor,Registry) getOrDefault [_uid, _actor];
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", "default", false]];
private _defaultActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]];
if !(_defaultActor isEqualType createHashMap) then {
_defaultActor = +_resolvedActor;
};
if (
(_defaultActor isEqualTo createHashMap)
|| { toLowerANSI (_defaultActor getOrDefault ["organization", ""]) isNotEqualTo "default" }
) then {
private _forcedActor = +_resolvedActor;
if (_forcedActor isEqualTo createHashMap) then {
_forcedActor = EGVAR(actor,ActorModel) call ["defaults", []];
_forcedActor set ["uid", _uid];
};
_forcedActor set ["organization", "default"];
_defaultActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]];
if (_defaultActor isEqualType createHashMap && { _defaultActor isNotEqualTo createHashMap }) then {
_actorPatch = createHashMapFromArray [["organization", "default"]];
};
};
if (
!(_defaultActor isEqualType createHashMap)
|| { _defaultActor isEqualTo createHashMap }
|| { toLowerANSI (_defaultActor getOrDefault ["organization", ""]) isNotEqualTo "default" }
) exitWith {
_result set ["message", "Failed to restore default organization membership."];
_result
};
private _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, _defaultActor]];
if (_defaultOrg isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to restore default organization membership."];
_result
};
_result set ["success", true];
_result set ["actorPatch", _actorPatch];
_result
}],
["leave", compileFinal {
params [["_uid", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["actorPatch", createHashMap],
["notification", []]
];
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required."];
_result
};
private _player = [_uid] call EFUNC(common,getPlayer);
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", ""];
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
_result set ["message", "You are already assigned to the default organization."];
_result
};
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Unable to load organization data for leave request."];
_result
};
private _ownerUid = _org getOrDefault ["owner", ""];
if (_ownerUid isEqualTo _uid) exitWith {
_result set ["message", "Organization owners must disband the organization instead of leaving it."];
_result
};
private _orgName = _org getOrDefault ["name", "Organization"];
private _updatedOrg = _self call ["removeMember", [_orgID, _uid]];
if (_updatedOrg isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to remove you from the organization roster."];
_result
};
private _defaultResult = _self call ["restoreDefaultMembership", [_uid, _player, _actor]];
if !(_defaultResult getOrDefault ["success", false]) exitWith {
_result set ["message", _defaultResult getOrDefault ["message", "Failed to restore default organization membership."]];
_result
};
private _message = format ["You left %1 and returned to the default organization.", _orgName];
_result set ["success", true];
_result set ["message", _message];
_result set ["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]];
_result set ["notification", ["info", "Organization Left", _message, 6000]];
_result
}],
["disband", compileFinal {
params [["_uid", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["members", []]
];
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required."];
_result
};
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", ""];
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
_result set ["message", "Only active player organizations can be disbanded."];
_result
};
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Unable to load organization data for disbanding."];
_result
};
private _ownerUid = _org getOrDefault ["owner", ""];
if (_ownerUid isEqualTo "" || { _ownerUid isNotEqualTo _uid }) exitWith {
_result set ["message", "Only the organization owner can disband this organization."];
_result
};
private _orgName = _org getOrDefault ["name", "Organization"];
private _memberMap = _org getOrDefault ["members", createHashMap];
private _memberUids = keys _memberMap;
if !(_uid in _memberUids) then {
_memberUids pushBack _uid;
};
private _deleteResult = GVAR(OrgStore) call ["delete", [_orgID]];
if !(_deleteResult getOrDefault ["success", false]) exitWith {
_result set ["message", _deleteResult getOrDefault ["message", "Failed to disband organization."]];
_result
};
private _memberResults = [];
{
private _memberUid = _x;
if (_memberUid isNotEqualTo "") then {
private _memberPlayer = [_memberUid] call EFUNC(common,getPlayer);
private _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap];
private _defaultResult = _self call ["restoreDefaultMembership", [_memberUid, _memberPlayer, _memberActor]];
if !(_defaultResult getOrDefault ["success", false]) then {
["WARNING", format ["Failed to restore default org for %1 after disbanding %2: %3", _memberUid, _orgID, _defaultResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log);
};
private _responseMessage = [
format ["%1 has been disbanded.", _orgName],
format ["Your organization, %1, has been disbanded.", _orgName]
] select (_memberUid isEqualTo _uid);
private _notificationParams = [
["warning", "Organization Disbanded", _responseMessage, 6000],
["success", "Organization Disbanded", _responseMessage, 6000]
] select (_memberUid isEqualTo _uid);
_memberResults pushBack (createHashMapFromArray [
["uid", _memberUid],
["requester", _memberUid isEqualTo _uid],
["message", _responseMessage],
["notification", _notificationParams],
["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]]
]);
};
} forEach _memberUids;
_result set ["success", true];
_result set ["message", format ["%1 has been disbanded.", _orgName]];
_result set ["members", _memberResults];
_result
}]
];
GVAR(OrgMembershipService) = createHashMapObject [GVAR(OrgMembershipServiceBase)];

View File

@ -1,166 +0,0 @@
#include "..\script_component.hpp"
#pragma hemtt ignore_variables ["_self"]
GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [
["#type", "OrgTreasuryService"],
["buildChargeResult", compileFinal {
params [["_message", "Unable to process organization payment.", [""]]];
createHashMapFromArray [
["success", false],
["message", _message],
["patch", createHashMap],
["memberUids", []]
]
}],
["resolveOrgMemberUids", compileFinal {
params [["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]]];
private _memberUids = keys (_org getOrDefault ["members", createHashMap]);
if (_requesterUid isNotEqualTo "" && { !(_requesterUid in _memberUids) }) then {
_memberUids pushBack _requesterUid;
};
_memberUids
}],
["canManageTreasury", compileFinal {
params [["_orgID", "", [""]], ["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]]];
private _ownerUid = _org getOrDefault ["owner", ""];
private _isDefaultOrg = (_orgID isEqualTo "default") || { toLowerANSI _ownerUid isEqualTo "server" };
private _isDefaultOrgCeo = _isDefaultOrg
&& { _requesterPlayer isNotEqualTo objNull }
&& { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" };
(_ownerUid isEqualTo _requesterUid) || _isDefaultOrgCeo
}],
["assignCreditLine", compileFinal {
params [["_requesterUid", "", [""]], ["_memberUid", "", [""]], ["_memberName", "", [""]], ["_amount", 0, [0]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["patch", createHashMap],
["memberUids", []]
];
if (
_requesterUid isEqualTo ""
|| { _memberUid isEqualTo "" }
|| { _amount <= 0 }
) exitWith {
_result set ["message", "A valid requester, member, and credit amount are required."];
_result
};
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
private _orgID = _requesterActor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Unable to load organization data for credit line assignment."];
_result
};
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
if !(_self call ["canManageTreasury", [_orgID, _org, _requesterUid, _requesterPlayer]]) exitWith {
_result set ["message", "Only the organization leader or CEO can manage treasury actions."];
_result
};
private _members = _org getOrDefault ["members", createHashMap];
private _memberRecord = _members getOrDefault [_memberUid, createHashMap];
if (_memberRecord isEqualTo createHashMap) exitWith {
_result set ["message", "Selected member was not found in the organization roster."];
_result
};
private _resolvedMemberName = _memberRecord getOrDefault ["name", _memberName];
if (_resolvedMemberName isEqualTo "") then { _resolvedMemberName = _memberName; };
private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]);
_creditLines set [_memberUid, createHashMapFromArray [
["uid", _memberUid],
["name", _resolvedMemberName],
["amount", _amount]
]];
private _patch = GVAR(OrgStore) call ["set", [_orgID, "credit_lines", _creditLines, false]];
private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]];
_result set ["success", true];
_result set ["message", format ["Credit line of $%1 assigned to %2.", [_amount] call EFUNC(common,formatNumber), _resolvedMemberName]];
_result set ["patch", _patch];
_result set ["memberUids", _memberUids];
_result
}],
["chargeCheckout", compileFinal {
params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]];
private _result = _self call ["buildChargeResult", []];
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
private _orgID = _requesterActor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Organization data is unavailable for checkout."];
_result
};
private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]];
switch (toLowerANSI _source) do {
case "org_funds": {
if !(_self call ["canManageTreasury", [_orgID, _org, _requesterUid, _requesterPlayer]]) exitWith {
_result set ["message", "Only the organization leader or CEO can charge org funds."];
_result
};
private _funds = _org getOrDefault ["funds", 0];
if (_funds < _amount) exitWith {
_result set ["message", "Organization funds cannot cover this checkout."];
_result
};
private _patch = createHashMapFromArray [["funds", (_funds - _amount)]];
if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [_orgID, _patch, false]]; };
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result set ["memberUids", _memberUids];
_result
};
case "credit_line": {
private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]);
private _memberCredit = +(_creditLines getOrDefault [_requesterUid, createHashMap]);
private _creditAmount = _memberCredit getOrDefault ["amount", 0];
if (_creditAmount < _amount) exitWith {
_result set ["message", "Assigned credit line cannot cover this checkout."];
_result
};
_memberCredit set ["uid", _requesterUid];
_memberCredit set ["amount", (_creditAmount - _amount)];
_creditLines set [_requesterUid, _memberCredit];
private _patch = createHashMapFromArray [["credit_lines", _creditLines]];
if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [_orgID, _patch, false]]; };
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result set ["memberUids", _memberUids];
_result
};
default {
_result set ["message", "Selected organization payment source is unsupported."];
_result
};
};
}]
];
GVAR(OrgTreasuryService) = createHashMapObject [GVAR(OrgTreasuryServiceBase)];

View File

@ -399,12 +399,20 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
_resolved
}],
["calculateCheckoutTotal", compileFinal {
["buildCheckoutRequest", compileFinal {
params [["_items", [], [[]]], ["_vehicles", [], [[]]]];
private _result = createHashMapFromArray [["success", false], ["total", 0], ["message", "Checkout total must be greater than zero."]];
private _result = createHashMapFromArray [
["success", false],
["total", 0],
["message", "Checkout total must be greater than zero."],
["items", []],
["vehicles", []]
];
private _total = 0;
private _message = "";
private _resolvedItems = [];
private _resolvedVehicles = [];
{
if (_message isEqualTo "") then {
@ -419,7 +427,14 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
if (_catalogEntry isEqualTo createHashMap) then {
_message = format ["Unsupported store item: %1", _className];
} else {
_total = _total + ((_catalogEntry getOrDefault ["priceValue", 0]) * _quantity);
private _priceValue = _catalogEntry getOrDefault ["priceValue", 0];
_total = _total + (_priceValue * _quantity);
_resolvedItems pushBack (createHashMapFromArray [
["classname", _className],
["category", _catalogEntry getOrDefault ["category", "item"]],
["priceValue", _priceValue],
["quantity", _quantity]
]);
};
};
};
@ -436,7 +451,13 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
if (_catalogEntry isEqualTo createHashMap) then {
_message = format ["Unsupported store vehicle: %1", _className];
} else {
_total = _total + (_catalogEntry getOrDefault ["priceValue", 0]);
private _priceValue = _catalogEntry getOrDefault ["priceValue", 0];
_total = _total + _priceValue;
_resolvedVehicles pushBack (createHashMapFromArray [
["classname", _className],
["category", _catalogEntry getOrDefault ["category", _x getOrDefault ["category", "other"]]],
["priceValue", _priceValue]
]);
};
};
};
@ -452,7 +473,19 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
_result set ["success", true];
_result set ["total", floor _total];
_result set ["message", ""];
_result set ["items", _resolvedItems];
_result set ["vehicles", _resolvedVehicles];
_result
}],
["calculateCheckoutTotal", compileFinal {
params [["_items", [], [[]]], ["_vehicles", [], [[]]]];
private _checkout = _self call ["buildCheckoutRequest", [_items, _vehicles]];
createHashMapFromArray [
["success", _checkout getOrDefault ["success", false]],
["total", _checkout getOrDefault ["total", 0]],
["message", _checkout getOrDefault ["message", "Checkout total must be greater than zero."]]
]
}]
];

View File

@ -160,45 +160,92 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
format ["$%1", [_amount max 0] call EFUNC(common,formatNumber)]
}],
["applyPaymentPatch", compileFinal {
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_paymentMethod", "cash", [""]], ["_total", 0, [0]], ["_commit", false, [false]]];
["callCheckoutBackendEnvelope", compileFinal {
params [["_context", createHashMap, [createHashMap]]];
private _result = _self call ["buildResult", ["Unable to process payment.", _paymentMethod]];
private _payment = switch (toLowerANSI _paymentMethod) do {
case "cash";
case "bank": {
EGVAR(bank,BankStore) call ["chargeCheckout", [_uid, _paymentMethod, _total, _commit]]
};
case "org_funds";
case "credit_line": {
EGVAR(org,OrgStore) call ["chargeCheckout", [_uid, _player, _paymentMethod, _total, _commit]]
};
default {
createHashMapFromArray [
["success", false],
["message", "Selected payment source is unsupported."],
["patch", createHashMap],
["memberUids", []]
]
};
private _envelope = createHashMapFromArray [["data", createHashMap], ["error", ""]];
if (_context isEqualTo createHashMap) exitWith {
_envelope set ["error", "Checkout request was invalid."];
_envelope
};
if !(_payment getOrDefault ["success", false]) exitWith {
_result set ["message", _payment getOrDefault ["message", "Unable to process payment."]];
_result
["store:checkout", [toJSON _context]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
_envelope set ["error", "Store backend call failed."];
_envelope
};
if !(_result isEqualType "") exitWith {
_envelope set ["error", "Store backend returned an invalid response."];
_envelope
};
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["Store extension checkout failed: %1", _result]] call EFUNC(common,log);
_envelope set ["error", _result select [7]];
_envelope
};
private _patch = _payment getOrDefault ["patch", createHashMap];
if ((_paymentMethod isEqualTo "cash") || { _paymentMethod isEqualTo "bank" }) then {
_result set ["bankPatch", _patch];
} else {
_result set ["orgPatch", _patch];
_result set ["orgTargetUids", _payment getOrDefault ["memberUids", []]];
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith {
_envelope set ["error", "Store backend returned unreadable JSON."];
_envelope
};
_result set ["success", true];
_result set ["message", ""];
_result
_envelope set ["data", _data];
_envelope
}],
["buildCheckoutContext", compileFinal {
params [
["_uid", "", [""]],
["_player", objNull, [objNull]],
["_paymentMethod", "cash", [""]],
["_items", [], [[]]],
["_vehicles", [], [[]]]
];
if (_uid isEqualTo "" || { isNull _player }) exitWith { createHashMap };
private _orgID = EGVAR(org,OrgStore) call ["resolveOrgIdForUid", [_uid]];
private _requesterIsDefaultOrgCeo = (
_orgID isEqualTo "default"
&& { toLowerANSI (vehicleVarName _player) isEqualTo "ceo" }
);
createHashMapFromArray [
["requesterUid", _uid],
["requesterName", name _player],
["orgId", _orgID],
["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo],
["paymentMethod", toLowerANSI _paymentMethod],
["items", _items],
["vehicles", _vehicles]
]
}],
["syncCheckoutResult", compileFinal {
params [["_player", objNull, [objNull]], ["_result", createHashMap, [createHashMap]]];
if (isNull _player || { _result isEqualTo createHashMap }) exitWith { false };
private _lockerPatch = _result getOrDefault ["lockerPatch", createHashMap];
private _vaPatch = _result getOrDefault ["vaPatch", createHashMap];
private _vgPatch = _result getOrDefault ["vgaragePatch", createHashMap];
private _bankPatch = _result getOrDefault ["bankPatch", createHashMap];
private _orgPatch = _result getOrDefault ["orgPatch", createHashMap];
if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); };
if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); };
if (keys _vgPatch isNotEqualTo []) then { [CRPC(garage,responseSyncVG), [_vgPatch], _player] call CFUNC(targetEvent); };
if (keys _bankPatch isNotEqualTo []) then { [CRPC(bank,responseSyncBank), [_bankPatch], _player] call CFUNC(targetEvent); };
if (keys _orgPatch isNotEqualTo []) then {
{
private _memberPlayer = [_x] call EFUNC(common,getPlayer);
if (_memberPlayer isNotEqualTo objNull) then {
[CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent);
};
} forEach (_result getOrDefault ["orgTargetUids", []]);
};
true
}],
["checkout", compileFinal {
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_payloadJson", "", [""]]];
@ -219,8 +266,8 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
_result
};
private _priceResult = GVAR(StoreCatalogService) call ["calculateCheckoutTotal", [_items, _vehicles]];
private _totalPrice = _priceResult getOrDefault ["total", 0];
private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles]];
private _totalPrice = _checkoutRequest getOrDefault ["total", 0];
_result set ["paymentMethod", _paymentMethod];
_result set ["chargedTotal", _totalPrice];
@ -230,90 +277,41 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
_result
};
if !(_priceResult getOrDefault ["success", false]) exitWith {
_result set ["message", _priceResult getOrDefault ["message", "Checkout total must be greater than zero."]];
if !(_checkoutRequest getOrDefault ["success", false]) exitWith {
_result set ["message", _checkoutRequest getOrDefault ["message", "Checkout total must be greater than zero."]];
_result
};
private _lockerPreview = EGVAR(locker,LockerStore) call ["grantItems", [_uid, _items, false]];
if !(_lockerPreview getOrDefault ["success", false]) exitWith {
_result set ["message", _lockerPreview getOrDefault ["message", "Locker grant failed."]];
private _checkoutContext = _self call ["buildCheckoutContext", [
_uid,
_player,
_paymentMethod,
_checkoutRequest getOrDefault ["items", []],
_checkoutRequest getOrDefault ["vehicles", []]
]];
if (_checkoutContext isEqualTo createHashMap) exitWith {
_result set ["message", "Checkout request context was invalid."];
_result
};
private _vaPreview = EGVAR(locker,VAStore) call ["unlockItems", [_uid, _items, false]];
if !(_vaPreview getOrDefault ["success", false]) exitWith {
_result set ["message", _vaPreview getOrDefault ["message", "VA unlock failed."]];
private _envelope = _self call ["callCheckoutBackendEnvelope", [_checkoutContext]];
private _backendResult = _envelope getOrDefault ["data", createHashMap];
if (_backendResult isEqualTo createHashMap) exitWith {
_result set ["message", _envelope getOrDefault ["error", "Checkout failed."]];
_result
};
private _vgPreview = EGVAR(garage,VGarageStore) call ["grantVehicles", [_uid, _vehicles, false]];
if !(_vgPreview getOrDefault ["success", false]) exitWith {
_result set ["message", _vgPreview getOrDefault ["message", "Vehicle unlock failed."]];
_result
};
private _orgFleetPreview = createHashMapFromArray [["success", true], ["message", ""], ["patch", createHashMap], ["memberUids", []]];
if (_paymentMethod isEqualTo "org_funds" && { _vehicles isNotEqualTo [] }) then {
_orgFleetPreview = EGVAR(org,OrgStore) call ["addFleetVehicles", [_uid, _vehicles, false]];
if !(_orgFleetPreview getOrDefault ["success", false]) exitWith {
_result set ["message", _orgFleetPreview getOrDefault ["message", "Organization fleet update failed."]];
_result
};
};
_result set ["lockerGranted", _lockerPreview getOrDefault ["granted", []]];
_result set ["vehicleGranted", _vgPreview getOrDefault ["granted", []]];
private _paymentPreview = _self call ["applyPaymentPatch", [_uid, _player, _paymentMethod, _totalPrice, false]];
if !(_paymentPreview getOrDefault ["success", false]) exitWith {
_result set ["message", _paymentPreview getOrDefault ["message", "Payment failed."]];
_result
};
private _payment = _self call ["applyPaymentPatch", [_uid, _player, _paymentMethod, _totalPrice, true]];
private _lockerResult = EGVAR(locker,LockerStore) call ["grantItems", [_uid, _items, true]];
private _vaResult = EGVAR(locker,VAStore) call ["unlockItems", [_uid, _items, true]];
private _vgResult = EGVAR(garage,VGarageStore) call ["grantVehicles", [_uid, _vehicles, true]];
private _orgFleetResult = createHashMapFromArray [["success", true], ["message", ""], ["patch", createHashMap], ["memberUids", []]];
if (_paymentMethod isEqualTo "org_funds" && { _vehicles isNotEqualTo [] }) then {
_orgFleetResult = EGVAR(org,OrgStore) call ["addFleetVehicles", [_uid, _vehicles, true]];
};
private _lockerPatch = _lockerResult getOrDefault ["patch", createHashMap];
private _vaPatch = _vaResult getOrDefault ["patch", createHashMap];
private _vgPatch = _vgResult getOrDefault ["patch", createHashMap];
if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); };
if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); };
if (keys _vgPatch isNotEqualTo []) then { [CRPC(garage,responseSyncVG), [_vgPatch], _player] call CFUNC(targetEvent); };
private _bankPatch = _payment getOrDefault ["bankPatch", createHashMap];
if (keys _bankPatch isNotEqualTo []) then { [CRPC(bank,responseSyncBank), [_bankPatch], _player] call CFUNC(targetEvent); };
private _orgPatch = _payment getOrDefault ["orgPatch", createHashMap];
private _orgFleetPatch = _orgFleetResult getOrDefault ["patch", createHashMap];
if (keys _orgFleetPatch isNotEqualTo []) then { { _orgPatch set [_x, _y]; } forEach _orgFleetPatch; };
if (keys _orgPatch isNotEqualTo []) then {
private _orgTargetUids = _payment getOrDefault ["orgTargetUids", []];
{
if !(_x in _orgTargetUids) then { _orgTargetUids pushBack _x; };
} forEach (_orgFleetResult getOrDefault ["memberUids", []]);
{
private _memberPlayer = [_x] call EFUNC(common,getPlayer);
if (_memberPlayer isNotEqualTo objNull) then { [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); };
} forEach _orgTargetUids;
};
_self call ["syncCheckoutResult", [_player, _backendResult]];
_result set ["success", true];
_result set ["message", format [
_result set ["message", _backendResult getOrDefault ["message", format [
"Checkout completed. %1 charged, %2 locker grant(s), %3 vehicle unlock(s).",
_self call ["formatCurrency", [_totalPrice]],
count (_lockerResult getOrDefault ["granted", []]),
count (_vgResult getOrDefault ["granted", []])
]];
_result set ["lockerGranted", _lockerResult getOrDefault ["granted", []]];
_result set ["vehicleGranted", _vgResult getOrDefault ["granted", []]];
count (_backendResult getOrDefault ["lockerGranted", []]),
count (_backendResult getOrDefault ["vehicleGranted", []])
]]];
_result set ["lockerGranted", _backendResult getOrDefault ["lockerGranted", []]];
_result set ["vehicleGranted", _backendResult getOrDefault ["vehicleGranted", []]];
_result
}]
];

View File

@ -31,6 +31,14 @@ static HOT_ACTOR_SERVICE: LazyLock<
ActorHotStateService::new(repository, hot_repository)
});
#[allow(dead_code)]
pub(crate) fn hot_service() -> &'static ActorHotStateService<
RedisActorRepository<ExtensionRedisClient>,
InMemoryActorHotRepository,
> {
&HOT_ACTOR_SERVICE
}
/// Creates the Arma 3 command group for actor operations.
///
/// Registers commands: `get`, `exists`, `create`, `update`, `delete`.

View File

@ -35,6 +35,13 @@ static HOT_BANK_SERVICE: LazyLock<
BankHotStateService::new(repository, hot_repository)
});
pub(crate) fn hot_service() -> &'static BankHotStateService<
RedisBankRepository<ExtensionRedisClient>,
InMemoryBankHotRepository,
> {
&HOT_BANK_SERVICE
}
/// Creates the Arma 3 command group for bank operations.
///
/// Registers commands: `get`, `exists`, `create`, `update`, `delete`.

View File

@ -30,6 +30,14 @@ static HOT_GARAGE_SERVICE: LazyLock<
GarageHotStateService::new(repository, hot_repository)
});
#[allow(dead_code)]
pub(crate) fn hot_service() -> &'static GarageHotStateService<
RedisGarageRepository<ExtensionRedisClient>,
InMemoryGarageHotRepository,
> {
&HOT_GARAGE_SERVICE
}
/// Creates the Arma 3 command group for garage operations.
///
/// Registers commands: `create`, `get`, `add`, `update`, `remove`, `delete`, `exists`.

View File

@ -22,6 +22,7 @@ pub mod locker;
mod log;
pub mod org;
pub mod redis;
pub mod store;
pub mod terrain;
pub mod transport;
pub mod v_garage;
@ -85,6 +86,7 @@ fn init() -> Extension {
.group("icom", icom::group())
.group("locker", locker::group())
.group("org", org::group())
.group("store", store::group())
.group("terrain", terrain::group())
.group("transport", transport::group())
.group(

View File

@ -25,6 +25,13 @@ static HOT_LOCKER_SERVICE: LazyLock<
LockerHotStateService::new(repository, hot_repository)
});
pub(crate) fn hot_service() -> &'static LockerHotStateService<
RedisLockerRepository<ExtensionRedisClient>,
InMemoryLockerHotRepository,
> {
&HOT_LOCKER_SERVICE
}
/// Creates the Arma 3 command group for locker operations.
///
/// Registers commands: `create`, `get`, `add`, `update`, `remove`, `delete`, `exists`.

View File

@ -4,7 +4,11 @@
//! Handles SQF command mapping and parameter validation.
use arma_rs::Group;
use forge_models::HotOrgRecord;
use forge_models::{
HotOrgRecord, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext, OrgDisbandResult,
OrgEnsureMemberContext, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext, OrgLeaveResult,
OrgRegisterContext,
};
use forge_repositories::{InMemoryOrgHotRepository, RedisOrgRepository};
use forge_services::{OrgHotStateService, OrgService};
use std::sync::LazyLock;
@ -31,6 +35,11 @@ static HOT_ORG_SERVICE: LazyLock<
OrgHotStateService::new(repository, hot_repository)
});
pub(crate) fn hot_service()
-> &'static OrgHotStateService<RedisOrgRepository<ExtensionRedisClient>, InMemoryOrgHotRepository> {
&HOT_ORG_SERVICE
}
/// Creates the Arma 3 command group for organization operations.
///
/// Registers commands: `get`, `exists`, `create`, `update`, `delete`.
@ -47,6 +56,14 @@ pub fn group() -> Group {
.command("init", init_hot_org)
.command("get", get_hot_org)
.command("override", override_hot_org)
.command("ensure_member", ensure_hot_org_member)
.command("register", register_hot_org)
.command("assign_credit_line", assign_credit_line_hot_org)
.command("charge_checkout", charge_checkout_hot_org)
.command("add_assets", add_assets_hot_org)
.command("add_fleet", add_fleet_hot_org)
.command("leave", leave_hot_org)
.command("disband", disband_hot_org)
.command("save", save_hot_org)
.command("remove", remove_hot_org),
)
@ -78,6 +95,13 @@ fn serialize_hot_org(org: HotOrgRecord) -> String {
}
}
fn serialize_result<T: serde::Serialize>(value: &T, label: &str) -> String {
match serde_json::to_string(value) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize {}: {}", label, error),
}
}
pub(crate) fn init_hot_org(org_id: String) -> String {
match HOT_ORG_SERVICE.init_org(org_id) {
Ok(org) => serialize_hot_org(org),
@ -104,6 +128,110 @@ pub(crate) fn override_hot_org(org_id: String, json_data: String) -> String {
}
}
pub(crate) fn ensure_hot_org_member(json_data: String) -> String {
let context: OrgEnsureMemberContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid ensure-member JSON: {}", error),
};
match HOT_ORG_SERVICE.ensure_member(context) {
Ok(org) => serialize_hot_org(org),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn register_hot_org(json_data: String) -> String {
let context: OrgRegisterContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid register org JSON: {}", error),
};
match HOT_ORG_SERVICE.register_org(context) {
Ok(result) => serialize_result(&result, "org register result"),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn assign_credit_line_hot_org(json_data: String) -> String {
let context: OrgCreditLineContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org credit-line JSON: {}", error),
};
match HOT_ORG_SERVICE.assign_credit_line(context) {
Ok(result) => serialize_result(&result, "org mutation result"),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn charge_checkout_hot_org(json_data: String) -> String {
let context: OrgCheckoutContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org checkout JSON: {}", error),
};
match HOT_ORG_SERVICE.charge_checkout(context) {
Ok(result) => serialize_result(&result, "org mutation result"),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn add_assets_hot_org(context_json: String, assets_json: String) -> String {
let context: OrgGrantContext = match serde_json::from_str(&context_json) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org asset context JSON: {}", error),
};
let assets: Vec<OrgAssetGrantSeed> = match serde_json::from_str(&assets_json) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org asset seed JSON: {}", error),
};
match HOT_ORG_SERVICE.add_assets(context, assets) {
Ok(result) => serialize_result(&result, "org mutation result"),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn add_fleet_hot_org(context_json: String, fleet_json: String) -> String {
let context: OrgGrantContext = match serde_json::from_str(&context_json) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org fleet context JSON: {}", error),
};
let fleet: Vec<OrgFleetGrantSeed> = match serde_json::from_str(&fleet_json) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org fleet seed JSON: {}", error),
};
match HOT_ORG_SERVICE.add_fleet_vehicles(context, fleet) {
Ok(result) => serialize_result(&result, "org mutation result"),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn leave_hot_org(json_data: String) -> String {
let context: OrgLeaveContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org leave JSON: {}", error),
};
match HOT_ORG_SERVICE.leave_org(context) {
Ok(result) => serialize_result::<OrgLeaveResult>(&result, "org leave result"),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn disband_hot_org(json_data: String) -> String {
let context: OrgLeaveContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org disband JSON: {}", error),
};
match HOT_ORG_SERVICE.disband_org(context) {
Ok(result) => serialize_result::<OrgDisbandResult>(&result, "org disband result"),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn save_hot_org(org_id: String) -> String {
match HOT_ORG_SERVICE.get_org(org_id.clone()) {
Ok(org) => {

View File

@ -0,0 +1,37 @@
use arma_rs::Group;
use forge_models::{StoreCheckoutContext, StoreCheckoutResult};
use forge_services::StoreService;
pub fn group() -> Group {
Group::new().command("checkout", checkout)
}
fn serialize_result(result: &StoreCheckoutResult) -> String {
match serde_json::to_string(result) {
Ok(json) => json,
Err(error) => format!(
"Error: Failed to serialize store checkout result: {}",
error
),
}
}
pub fn checkout(json_data: String) -> String {
let context: StoreCheckoutContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid store checkout JSON: {}", error),
};
let service = StoreService::new(
crate::bank::hot_service(),
crate::org::hot_service(),
crate::locker::hot_service(),
crate::v_locker::hot_service(),
crate::v_garage::hot_service(),
);
match service.checkout(context) {
Ok(result) => serialize_result(&result),
Err(error) => format!("Error: {}", error),
}
}

View File

@ -351,6 +351,44 @@ fn route_command(
arguments[1].clone(),
))
}
"org:hot:ensure_member" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::ensure_hot_org_member(arguments[0].clone()))
}
"org:hot:register" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::register_hot_org(arguments[0].clone()))
}
"org:hot:assign_credit_line" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::assign_credit_line_hot_org(arguments[0].clone()))
}
"org:hot:charge_checkout" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::charge_checkout_hot_org(arguments[0].clone()))
}
"org:hot:add_assets" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::add_assets_hot_org(
arguments[0].clone(),
arguments[1].clone(),
))
}
"org:hot:add_fleet" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::add_fleet_hot_org(
arguments[0].clone(),
arguments[1].clone(),
))
}
"org:hot:leave" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::leave_hot_org(arguments[0].clone()))
}
"org:hot:disband" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::disband_hot_org(arguments[0].clone()))
}
"org:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::save_hot_org(arguments[0].clone()))
@ -396,6 +434,10 @@ fn route_command(
arguments[1].clone(),
))
}
"store:checkout" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(crate::store::checkout(arguments[0].clone()))
}
"garage:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::create_garage(call_context, arguments[0].clone()))

View File

@ -27,6 +27,13 @@ static HOT_VGARAGE_SERVICE: LazyLock<
VGarageHotStateService::new(repository, hot_repository)
});
pub(crate) fn hot_service() -> &'static VGarageHotStateService<
RedisVGarageRepository<ExtensionRedisClient>,
InMemoryVGarageHotRepository,
> {
&HOT_VGARAGE_SERVICE
}
/// Creates the Arma 3 command group for virtual garage operations.
///
/// Registers commands: `create`, `fetch`, `get`, `add`, `remove`, `delete`, `exists`.

View File

@ -27,6 +27,13 @@ static HOT_VLOCKER_SERVICE: LazyLock<
VLockerHotStateService::new(repository, hot_repository)
});
pub(crate) fn hot_service() -> &'static VLockerHotStateService<
RedisVLockerRepository<ExtensionRedisClient>,
InMemoryVLockerHotRepository,
> {
&HOT_VLOCKER_SERVICE
}
/// Creates the Arma 3 command group for virtual locker operations.
///
/// Registers commands: `create`, `fetch`, `get`, `add`, `remove`, `delete`, `exists`.

View File

@ -4,6 +4,7 @@ pub mod cad;
pub mod garage;
pub mod locker;
pub mod org;
pub mod store;
pub mod v_garage;
pub mod v_locker;
@ -20,6 +21,15 @@ pub use cad::{
};
pub use garage::{Garage, HitPoints, Vehicle};
pub use locker::{Item, Locker};
pub use org::{CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
pub use org::{
CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgAssetGrantSeed,
OrgCheckoutContext, OrgCreditLineContext, OrgDisbandMemberResult, OrgDisbandResult,
OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext,
OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult,
};
pub use store::{
StoreCheckoutContext, StoreCheckoutItemSeed, StoreCheckoutResult, StoreCheckoutVehicleSeed,
StoreGrantedItem, StoreGrantedVehicle,
};
pub use v_garage::{VGarage, VehicleCategory};
pub use v_locker::{EquipmentCategory, VLocker};

View File

@ -65,6 +65,117 @@ pub struct HotOrgRecord {
pub members: HashMap<String, MemberSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgEnsureMemberContext {
pub org_id: String,
pub member_uid: String,
pub member_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgRegisterContext {
pub requester_uid: String,
pub requester_name: String,
pub org_id: String,
pub org_name: String,
pub existing_org_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgRegisterResult {
pub org: HotOrgRecord,
pub actor_organization: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgCreditLineContext {
pub requester_uid: String,
pub org_id: String,
pub requester_is_default_org_ceo: bool,
pub member_uid: String,
pub member_name: String,
pub amount: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgCheckoutContext {
pub requester_uid: String,
pub org_id: String,
pub requester_is_default_org_ceo: bool,
pub source: String,
pub amount: f64,
pub commit: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgAssetGrantSeed {
pub classname: String,
pub category: String,
pub quantity: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgFleetGrantSeed {
pub classname: String,
pub category: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgGrantContext {
pub requester_uid: String,
pub org_id: String,
pub commit: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgMutationResult {
pub org: HotOrgRecord,
pub patch: HashMap<String, serde_json::Value>,
pub member_uids: Vec<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgLeaveContext {
pub requester_uid: String,
pub requester_name: String,
pub org_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgLeaveResult {
pub actor_organization: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgDisbandMemberResult {
pub uid: String,
pub requester: bool,
pub actor_organization: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgDisbandResult {
pub message: String,
pub members: Vec<OrgDisbandMemberResult>,
}
impl Org {
pub fn new<S: Into<String>>(id: S, owner: S, name: S) -> Result<Self, OrgValidationError> {
let org = Self {

72
lib/models/src/store.rs Normal file
View File

@ -0,0 +1,72 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreCheckoutItemSeed {
pub classname: String,
pub category: String,
pub price_value: f64,
pub quantity: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreCheckoutVehicleSeed {
pub classname: String,
pub category: String,
pub price_value: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreCheckoutContext {
pub requester_uid: String,
pub requester_name: String,
pub org_id: String,
pub requester_is_default_org_ceo: bool,
pub payment_method: String,
#[serde(default)]
pub items: Vec<StoreCheckoutItemSeed>,
#[serde(default)]
pub vehicles: Vec<StoreCheckoutVehicleSeed>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreGrantedItem {
pub classname: String,
pub category: String,
pub quantity: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreGrantedVehicle {
pub classname: String,
pub category: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreCheckoutResult {
pub charged_total: f64,
pub payment_method: String,
pub message: String,
#[serde(default)]
pub locker_granted: Vec<StoreGrantedItem>,
#[serde(default)]
pub vehicle_granted: Vec<StoreGrantedVehicle>,
#[serde(default)]
pub locker_patch: HashMap<String, serde_json::Value>,
#[serde(default)]
pub va_patch: HashMap<String, serde_json::Value>,
#[serde(default)]
pub vgarage_patch: HashMap<String, serde_json::Value>,
#[serde(default)]
pub bank_patch: HashMap<String, serde_json::Value>,
#[serde(default)]
pub org_patch: HashMap<String, serde_json::Value>,
#[serde(default)]
pub org_target_uids: Vec<String>,
}

View File

@ -4,6 +4,7 @@ pub mod cad;
pub mod garage;
pub mod locker;
pub mod org;
pub mod store;
pub mod v_garage;
pub mod v_locker;
@ -13,5 +14,6 @@ pub use cad::{CadStateService, CadViewService};
pub use garage::{GarageHotStateService, GarageService};
pub use locker::{LockerHotStateService, LockerService};
pub use org::{OrgHotStateService, OrgService};
pub use store::StoreService;
pub use v_garage::{VGarageHotStateService, VGarageService};
pub use v_locker::{VLockerHotStateService, VLockerService};

View File

@ -6,9 +6,13 @@
//! For full documentation, architecture, and examples, see the [crate README](../README.md).
use forge_models::{
CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry,
CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgAssetGrantSeed,
OrgCheckoutContext, OrgCreditLineContext, OrgDisbandMemberResult, OrgDisbandResult,
OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext,
OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult,
};
use forge_repositories::{OrgHotRepository, OrgRepository};
use serde_json::{Value, json};
use std::collections::{HashMap, HashSet};
/// Service layer implementation for organization business logic and operations.
@ -328,6 +332,19 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
pub fn init_org(&self, id: String) -> Result<HotOrgRecord, String> {
if let Some(org) = self.repository.get(&id)? {
if !org.members.is_empty() || !org.assets.is_empty() || !org.fleet.is_empty() {
return Ok(org);
}
let hydrated_org = self.hydrate_org(&id)?;
if !hydrated_org.members.is_empty()
|| !hydrated_org.assets.is_empty()
|| !hydrated_org.fleet.is_empty()
{
self.repository.save(&hydrated_org)?;
return Ok(hydrated_org);
}
return Ok(org);
}
@ -392,6 +409,428 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
self.repository.delete(&id)
}
pub fn ensure_member(&self, context: OrgEnsureMemberContext) -> Result<HotOrgRecord, String> {
if context.org_id.trim().is_empty() || context.member_uid.trim().is_empty() {
return Err("A valid organization and member UID are required.".to_string());
}
let mut org = self.get_org(context.org_id)?;
if !org.members.contains_key(&context.member_uid) {
let member_name = if context.member_name.trim().is_empty() {
"Unknown".to_string()
} else {
context.member_name
};
org.members.insert(
context.member_uid.clone(),
MemberSummary {
uid: context.member_uid,
name: member_name,
},
);
self.repository.save(&org)?;
}
Ok(org)
}
pub fn register_org(&self, context: OrgRegisterContext) -> Result<OrgRegisterResult, String> {
if context.requester_uid.trim().is_empty() || context.org_id.trim().is_empty() {
return Err("A valid requester and organization ID are required.".to_string());
}
if context.org_name.trim().is_empty() {
return Err("Organization name cannot be empty.".to_string());
}
if !context.existing_org_id.trim().is_empty()
&& !context.existing_org_id.eq_ignore_ascii_case("default")
{
return Err("Player already belongs to an organization.".to_string());
}
if self.service.org_exists(context.org_id.clone())?
|| self.repository.get(&context.org_id)?.is_some()
{
return Err("An organization already exists for this phone number.".to_string());
}
let org = Org {
id: context.org_id.clone(),
owner: context.requester_uid.clone(),
name: context.org_name,
funds: 0.0,
reputation: 0,
credit_lines: HashMap::new(),
};
org.validate()
.map_err(|error| format!("Validation failed: {}", error))?;
let json_data = serde_json::to_string(&org)
.map_err(|error| format!("Failed to serialize org: {}", error))?;
let persisted_org = self.service.create_org(context.org_id.clone(), json_data)?;
let mut hot_org =
HotOrgRecord::from_parts(persisted_org, HashMap::new(), HashMap::new(), Vec::new());
hot_org.members.insert(
context.requester_uid.clone(),
MemberSummary {
uid: context.requester_uid.clone(),
name: if context.requester_name.trim().is_empty() {
"Unknown".to_string()
} else {
context.requester_name
},
},
);
self.repository.save(&hot_org)?;
if context.existing_org_id.eq_ignore_ascii_case("default") {
let mut default_org = self.init_org("default".to_string())?;
default_org.members.remove(&context.requester_uid);
self.repository.save(&default_org)?;
}
Ok(OrgRegisterResult {
org: hot_org,
actor_organization: context.org_id,
message: String::new(),
})
}
pub fn assign_credit_line(
&self,
context: OrgCreditLineContext,
) -> Result<OrgMutationResult, String> {
if context.requester_uid.trim().is_empty()
|| context.member_uid.trim().is_empty()
|| context.org_id.trim().is_empty()
{
return Err("A valid requester, member, and organization are required.".to_string());
}
if context.amount <= 0.0 {
return Err("A valid credit amount is required.".to_string());
}
let mut org = self.get_org(context.org_id)?;
if !can_manage_treasury(
&org,
&context.requester_uid,
context.requester_is_default_org_ceo,
) {
return Err(
"Only the organization leader or CEO can manage treasury actions.".to_string(),
);
}
let member_record = org
.members
.get(&context.member_uid)
.cloned()
.ok_or_else(|| {
"Selected member was not found in the organization roster.".to_string()
})?;
let member_name = if context.member_name.trim().is_empty() {
member_record.name
} else {
context.member_name
};
org.credit_lines.insert(
context.member_uid.clone(),
CreditLineSummary {
uid: context.member_uid.clone(),
name: member_name.clone(),
amount: context.amount,
},
);
self.repository.save(&org)?;
Ok(OrgMutationResult {
patch: build_org_patch(&org, &["credit_lines"])?,
member_uids: resolve_member_uids(&org, Some(&context.requester_uid)),
message: format!(
"Credit line of ${} assigned to {}.",
format_currency(context.amount),
member_name
),
org,
})
}
pub fn charge_checkout(
&self,
context: OrgCheckoutContext,
) -> Result<OrgMutationResult, String> {
if context.requester_uid.trim().is_empty() || context.org_id.trim().is_empty() {
return Err("A valid requester and organization are required.".to_string());
}
if context.amount <= 0.0 {
return Err("Checkout amount must be greater than zero.".to_string());
}
let mut org = self.get_org(context.org_id)?;
let member_uids = resolve_member_uids(&org, Some(&context.requester_uid));
match context.source.trim().to_ascii_lowercase().as_str() {
"org_funds" => {
if !can_manage_treasury(
&org,
&context.requester_uid,
context.requester_is_default_org_ceo,
) {
return Err(
"Only the organization leader or CEO can charge org funds.".to_string()
);
}
if org.funds < context.amount {
return Err("Organization funds cannot cover this checkout.".to_string());
}
org.funds -= context.amount;
self.repository.save(&org)?;
Ok(OrgMutationResult {
patch: build_org_patch(&org, &["funds"])?,
member_uids,
message: String::new(),
org,
})
}
"credit_line" => {
let mut credit_line = org
.credit_lines
.get(&context.requester_uid)
.cloned()
.ok_or_else(|| {
"Assigned credit line cannot cover this checkout.".to_string()
})?;
if credit_line.amount < context.amount {
return Err("Assigned credit line cannot cover this checkout.".to_string());
}
credit_line.amount -= context.amount;
org.credit_lines
.insert(context.requester_uid.clone(), credit_line);
self.repository.save(&org)?;
Ok(OrgMutationResult {
patch: build_org_patch(&org, &["credit_lines"])?,
member_uids,
message: String::new(),
org,
})
}
_ => Err("Selected organization payment source is unsupported.".to_string()),
}
}
pub fn add_assets(
&self,
context: OrgGrantContext,
assets: Vec<OrgAssetGrantSeed>,
) -> Result<OrgMutationResult, String> {
if context.org_id.trim().is_empty() {
return Err("A valid organization is required for asset updates.".to_string());
}
if assets.is_empty() {
let org = self.get_org(context.org_id)?;
return Ok(OrgMutationResult {
org,
patch: HashMap::new(),
member_uids: Vec::new(),
message: String::new(),
});
}
let mut org = self.get_org(context.org_id)?;
for asset in assets {
if asset.classname.trim().is_empty() || asset.quantity <= 0 {
continue;
}
let category = asset.category.trim().to_ascii_lowercase();
let category_assets = org.assets.entry(category.clone()).or_default();
let entry = category_assets
.entry(asset.classname.clone())
.or_insert_with(|| OrgAssetEntry {
classname: asset.classname.clone(),
asset_type: category.clone(),
quantity: 0,
});
entry.quantity += asset.quantity;
}
self.repository.save(&org)?;
Ok(OrgMutationResult {
patch: build_org_patch(&org, &["assets"])?,
member_uids: resolve_member_uids(&org, Some(&context.requester_uid)),
message: String::new(),
org,
})
}
pub fn add_fleet_vehicles(
&self,
context: OrgGrantContext,
vehicles: Vec<OrgFleetGrantSeed>,
) -> Result<OrgMutationResult, String> {
if context.org_id.trim().is_empty() {
return Err("A valid organization is required for fleet updates.".to_string());
}
if vehicles.is_empty() {
let org = self.get_org(context.org_id)?;
return Ok(OrgMutationResult {
org,
patch: HashMap::new(),
member_uids: Vec::new(),
message: String::new(),
});
}
let mut org = self.get_org(context.org_id)?;
let mut fleet_index = org.fleet.len();
for vehicle in vehicles {
if vehicle.classname.trim().is_empty() {
continue;
}
let fleet_type = vehicle.category.trim().to_ascii_lowercase();
let mut fleet_key = format!("{}_{}", vehicle.classname, fleet_index);
while org.fleet.contains_key(&fleet_key) {
fleet_index += 1;
fleet_key = format!("{}_{}", vehicle.classname, fleet_index);
}
org.fleet.insert(
fleet_key,
OrgFleetEntry {
classname: vehicle.classname.clone(),
name: vehicle.classname,
fleet_type,
status: "Ready".to_string(),
damage: "0%".to_string(),
},
);
fleet_index += 1;
}
self.repository.save(&org)?;
Ok(OrgMutationResult {
patch: build_org_patch(&org, &["fleet"])?,
member_uids: resolve_member_uids(&org, Some(&context.requester_uid)),
message: String::new(),
org,
})
}
pub fn leave_org(&self, context: OrgLeaveContext) -> Result<OrgLeaveResult, String> {
if context.requester_uid.trim().is_empty() {
return Err("A valid player UID is required.".to_string());
}
if context.org_id.trim().is_empty() || context.org_id.eq_ignore_ascii_case("default") {
return Err("You are already assigned to the default organization.".to_string());
}
let mut org = self.get_org(context.org_id)?;
if org.owner == context.requester_uid {
return Err(
"Organization owners must disband the organization instead of leaving it."
.to_string(),
);
}
let org_name = org.name.clone();
org.members.remove(&context.requester_uid);
self.repository.save(&org)?;
let mut default_org = self.init_org("default".to_string())?;
let requester_uid = context.requester_uid.clone();
default_org.members.insert(
requester_uid.clone(),
MemberSummary {
uid: requester_uid,
name: if context.requester_name.trim().is_empty() {
"Unknown".to_string()
} else {
context.requester_name
},
},
);
self.repository.save(&default_org)?;
Ok(OrgLeaveResult {
actor_organization: "default".to_string(),
message: format!(
"You left {} and returned to the default organization.",
org_name
),
})
}
pub fn disband_org(&self, context: OrgLeaveContext) -> Result<OrgDisbandResult, String> {
if context.requester_uid.trim().is_empty() {
return Err("A valid player UID is required.".to_string());
}
if context.org_id.trim().is_empty() || context.org_id.eq_ignore_ascii_case("default") {
return Err("Only active player organizations can be disbanded.".to_string());
}
let org = self.get_org(context.org_id.clone())?;
if org.owner != context.requester_uid {
return Err("Only the organization owner can disband this organization.".to_string());
}
let org_name = org.name.clone();
let mut default_org = self.init_org("default".to_string())?;
let mut member_results = Vec::new();
let mut seen = HashSet::new();
for (member_uid, member) in &org.members {
if seen.insert(member_uid.clone()) {
default_org
.members
.insert(member_uid.clone(), member.clone());
member_results.push(OrgDisbandMemberResult {
uid: member_uid.clone(),
requester: member_uid == &context.requester_uid,
actor_organization: "default".to_string(),
message: if member_uid == &context.requester_uid {
format!("Your organization, {}, has been disbanded.", org_name)
} else {
format!("{} has been disbanded.", org_name)
},
});
}
}
if seen.insert(context.requester_uid.clone()) {
default_org.members.insert(
context.requester_uid.clone(),
MemberSummary {
uid: context.requester_uid.clone(),
name: if context.requester_name.trim().is_empty() {
"Unknown".to_string()
} else {
context.requester_name
},
},
);
member_results.push(OrgDisbandMemberResult {
uid: context.requester_uid,
requester: true,
actor_organization: "default".to_string(),
message: format!("Your organization, {}, has been disbanded.", org_name),
});
}
self.repository.save(&default_org)?;
self.service.delete_org(context.org_id.clone())?;
self.repository.delete(&context.org_id)?;
Ok(OrgDisbandResult {
message: format!("{} has been disbanded.", org_name),
members: member_results,
})
}
fn hydrate_org(&self, id: &str) -> Result<HotOrgRecord, String> {
let org = self
.service
@ -403,3 +842,65 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
Ok(HotOrgRecord::from_parts(org, assets, fleet, members))
}
}
fn can_manage_treasury(
org: &HotOrgRecord,
requester_uid: &str,
requester_is_default_org_ceo: bool,
) -> bool {
org.owner == requester_uid
|| ((org.id.eq_ignore_ascii_case("default") || org.owner.eq_ignore_ascii_case("server"))
&& requester_is_default_org_ceo)
}
fn resolve_member_uids(org: &HotOrgRecord, requester_uid: Option<&str>) -> Vec<String> {
let mut member_uids = org.members.keys().cloned().collect::<Vec<_>>();
if let Some(uid) = requester_uid {
if !uid.is_empty() && !member_uids.iter().any(|member_uid| member_uid == uid) {
member_uids.push(uid.to_string());
}
}
member_uids
}
fn build_org_patch(org: &HotOrgRecord, fields: &[&str]) -> Result<HashMap<String, Value>, String> {
let mut patch = HashMap::new();
for field in fields {
patch.insert((*field).to_string(), current_org_field_value(org, field)?);
}
Ok(patch)
}
fn current_org_field_value(org: &HotOrgRecord, field: &str) -> Result<Value, String> {
match field {
"id" => Ok(json!(org.id)),
"owner" => Ok(json!(org.owner)),
"name" => Ok(json!(org.name)),
"funds" => Ok(json!(org.funds)),
"reputation" => Ok(json!(org.reputation)),
"credit_lines" => serde_json::to_value(&org.credit_lines)
.map_err(|error| format!("Failed to serialize org credit lines: {}", error)),
"assets" => serde_json::to_value(&org.assets)
.map_err(|error| format!("Failed to serialize org assets: {}", error)),
"fleet" => serde_json::to_value(&org.fleet)
.map_err(|error| format!("Failed to serialize org fleet: {}", error)),
"members" => serde_json::to_value(&org.members)
.map_err(|error| format!("Failed to serialize org members: {}", error)),
_ => Err(format!("Unknown field: {}", field)),
}
}
fn format_currency(amount: f64) -> String {
let rounded = amount.max(0.0).round() as i64;
let digits = rounded.to_string();
let mut formatted = String::new();
for (index, character) in digits.chars().rev().enumerate() {
if index > 0 && index % 3 == 0 {
formatted.push(',');
}
formatted.push(character);
}
formatted.chars().rev().collect()
}

685
lib/services/src/store.rs Normal file
View File

@ -0,0 +1,685 @@
use forge_models::{
Bank, BankCheckoutContext, BankMutationResult, EquipmentCategory, HotOrgRecord, Item, Locker,
OrgFleetEntry, StoreCheckoutContext, StoreCheckoutResult, StoreGrantedItem,
StoreGrantedVehicle, VGarage, VLocker, VehicleCategory,
};
use forge_repositories::{
BankHotRepository, BankRepository, LockerHotRepository, LockerRepository, OrgHotRepository,
OrgRepository, VGarageHotRepository, VGarageRepository, VLockerHotRepository,
VLockerRepository,
};
use serde_json::json;
use std::collections::HashMap;
use crate::{
BankHotStateService, LockerHotStateService, OrgHotStateService, VGarageHotStateService,
VLockerHotStateService,
};
pub trait StoreBankBackend {
fn get_bank(&self, uid: &str) -> Result<Bank, String>;
fn preview_checkout(
&self,
uid: &str,
amount: f64,
source: &str,
) -> Result<BankMutationResult, String>;
fn override_bank(&self, uid: &str, bank: &Bank) -> Result<Bank, String>;
}
pub trait StoreOrgBackend {
fn get_org(&self, org_id: &str) -> Result<HotOrgRecord, String>;
fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result<HotOrgRecord, String>;
}
pub trait StoreLockerBackend {
fn get_locker(&self, uid: &str) -> Result<Locker, String>;
fn override_locker(&self, uid: &str, items: HashMap<String, Item>) -> Result<Locker, String>;
}
pub trait StoreVLockerBackend {
fn fetch_locker(&self, uid: &str) -> Result<VLocker, String>;
fn override_locker(&self, uid: &str, locker: VLocker) -> Result<VLocker, String>;
}
pub trait StoreVGarageBackend {
fn fetch_garage(&self, uid: &str) -> Result<VGarage, String>;
fn override_garage(&self, uid: &str, garage: VGarage) -> Result<VGarage, String>;
}
impl<R: BankRepository, H: BankHotRepository> StoreBankBackend for BankHotStateService<R, H> {
fn get_bank(&self, uid: &str) -> Result<Bank, String> {
BankHotStateService::get_bank(self, uid.to_string())
}
fn preview_checkout(
&self,
uid: &str,
amount: f64,
source: &str,
) -> Result<BankMutationResult, String> {
BankHotStateService::charge_checkout(
self,
uid.to_string(),
amount,
BankCheckoutContext {
source_field: source.to_string(),
commit: false,
},
)
}
fn override_bank(&self, uid: &str, bank: &Bank) -> Result<Bank, String> {
let json = serde_json::to_string(bank)
.map_err(|error| format!("Invalid bank override JSON: {}", error))?;
BankHotStateService::override_bank(self, uid.to_string(), json)
}
}
impl<R: BankRepository, H: BankHotRepository> StoreBankBackend for &BankHotStateService<R, H> {
fn get_bank(&self, uid: &str) -> Result<Bank, String> {
BankHotStateService::get_bank(self, uid.to_string())
}
fn preview_checkout(
&self,
uid: &str,
amount: f64,
source: &str,
) -> Result<BankMutationResult, String> {
BankHotStateService::charge_checkout(
self,
uid.to_string(),
amount,
BankCheckoutContext {
source_field: source.to_string(),
commit: false,
},
)
}
fn override_bank(&self, uid: &str, bank: &Bank) -> Result<Bank, String> {
let json = serde_json::to_string(bank)
.map_err(|error| format!("Invalid bank override JSON: {}", error))?;
BankHotStateService::override_bank(self, uid.to_string(), json)
}
}
impl<R: OrgRepository, H: OrgHotRepository> StoreOrgBackend for OrgHotStateService<R, H> {
fn get_org(&self, org_id: &str) -> Result<HotOrgRecord, String> {
OrgHotStateService::get_org(self, org_id.to_string())
}
fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result<HotOrgRecord, String> {
OrgHotStateService::override_org(self, org_id.to_string(), org)
}
}
impl<R: OrgRepository, H: OrgHotRepository> StoreOrgBackend for &OrgHotStateService<R, H> {
fn get_org(&self, org_id: &str) -> Result<HotOrgRecord, String> {
OrgHotStateService::get_org(self, org_id.to_string())
}
fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result<HotOrgRecord, String> {
OrgHotStateService::override_org(self, org_id.to_string(), org)
}
}
impl<R: LockerRepository, H: LockerHotRepository> StoreLockerBackend
for LockerHotStateService<R, H>
{
fn get_locker(&self, uid: &str) -> Result<Locker, String> {
LockerHotStateService::get_locker(self, uid.to_string())
}
fn override_locker(&self, uid: &str, items: HashMap<String, Item>) -> Result<Locker, String> {
LockerHotStateService::override_locker(self, uid.to_string(), items)
}
}
impl<R: LockerRepository, H: LockerHotRepository> StoreLockerBackend
for &LockerHotStateService<R, H>
{
fn get_locker(&self, uid: &str) -> Result<Locker, String> {
LockerHotStateService::get_locker(self, uid.to_string())
}
fn override_locker(&self, uid: &str, items: HashMap<String, Item>) -> Result<Locker, String> {
LockerHotStateService::override_locker(self, uid.to_string(), items)
}
}
impl<R: VLockerRepository, H: VLockerHotRepository> StoreVLockerBackend
for VLockerHotStateService<R, H>
{
fn fetch_locker(&self, uid: &str) -> Result<VLocker, String> {
VLockerHotStateService::fetch_locker(self, uid)
}
fn override_locker(&self, uid: &str, locker: VLocker) -> Result<VLocker, String> {
VLockerHotStateService::override_locker(self, uid, locker)
}
}
impl<R: VLockerRepository, H: VLockerHotRepository> StoreVLockerBackend
for &VLockerHotStateService<R, H>
{
fn fetch_locker(&self, uid: &str) -> Result<VLocker, String> {
VLockerHotStateService::fetch_locker(self, uid)
}
fn override_locker(&self, uid: &str, locker: VLocker) -> Result<VLocker, String> {
VLockerHotStateService::override_locker(self, uid, locker)
}
}
impl<R: VGarageRepository, H: VGarageHotRepository> StoreVGarageBackend
for VGarageHotStateService<R, H>
{
fn fetch_garage(&self, uid: &str) -> Result<VGarage, String> {
VGarageHotStateService::fetch_garage(self, uid)
}
fn override_garage(&self, uid: &str, garage: VGarage) -> Result<VGarage, String> {
VGarageHotStateService::override_garage(self, uid, garage)
}
}
impl<R: VGarageRepository, H: VGarageHotRepository> StoreVGarageBackend
for &VGarageHotStateService<R, H>
{
fn fetch_garage(&self, uid: &str) -> Result<VGarage, String> {
VGarageHotStateService::fetch_garage(self, uid)
}
fn override_garage(&self, uid: &str, garage: VGarage) -> Result<VGarage, String> {
VGarageHotStateService::override_garage(self, uid, garage)
}
}
pub struct StoreService<B, O, L, VL, VG> {
bank: B,
org: O,
locker: L,
vlocker: VL,
vgarage: VG,
}
impl<B, O, L, VL, VG> StoreService<B, O, L, VL, VG> {
pub fn new(bank: B, org: O, locker: L, vlocker: VL, vgarage: VG) -> Self {
Self {
bank,
org,
locker,
vlocker,
vgarage,
}
}
}
impl<B, O, L, VL, VG> StoreService<B, O, L, VL, VG>
where
B: StoreBankBackend,
O: StoreOrgBackend,
L: StoreLockerBackend,
VL: StoreVLockerBackend,
VG: StoreVGarageBackend,
{
pub fn checkout(&self, context: StoreCheckoutContext) -> Result<StoreCheckoutResult, String> {
if context.requester_uid.trim().is_empty() {
return Err("A valid requester UID is required.".to_string());
}
if context.items.is_empty() && context.vehicles.is_empty() {
return Err("Add at least one item before checkout.".to_string());
}
let charged_total = checkout_total(&context);
if charged_total <= 0.0 {
return Err("Checkout total must be greater than zero.".to_string());
}
let requester_uid = context.requester_uid.trim();
let payment_method = context.payment_method.trim().to_ascii_lowercase();
let original_locker = self.locker.get_locker(requester_uid)?;
let original_vlocker = self.vlocker.fetch_locker(requester_uid)?;
let original_vgarage = self.vgarage.fetch_garage(requester_uid)?;
let mut next_locker = original_locker.clone();
let mut next_vlocker = original_vlocker.clone();
let mut next_vgarage = original_vgarage.clone();
let mut locker_patch = HashMap::new();
let mut va_patch = HashMap::new();
let mut vgarage_patch = HashMap::new();
let mut locker_granted = Vec::new();
let mut vehicle_granted = Vec::new();
let mut va_categories_changed: Vec<&str> = Vec::new();
let mut vgarage_categories_changed: Vec<&str> = Vec::new();
for item_seed in &context.items {
if item_seed.classname.trim().is_empty() || item_seed.quantity == 0 {
return Err("Checkout contains an invalid item entry.".to_string());
}
let locker_category = resolve_locker_category(&item_seed.category)?;
let arsenal_category = resolve_arsenal_category(&item_seed.category)?;
let existing_amount = next_locker
.items
.get(&item_seed.classname)
.map(|entry| entry.amount)
.unwrap_or(0);
let updated_item = Item {
category: locker_category.to_string(),
classname: item_seed.classname.clone(),
amount: existing_amount.saturating_add(item_seed.quantity),
};
next_locker
.items
.insert(item_seed.classname.clone(), updated_item.clone());
locker_patch.insert(
item_seed.classname.clone(),
serde_json::to_value(&updated_item)
.map_err(|error| format!("Failed to serialize locker patch: {}", error))?,
);
locker_granted.push(StoreGrantedItem {
classname: item_seed.classname.clone(),
category: locker_category.to_string(),
quantity: item_seed.quantity,
});
match arsenal_category {
EquipmentCategory::Items => {
push_unique(&mut next_vlocker.items, &item_seed.classname);
push_unique_str(&mut va_categories_changed, "items");
}
EquipmentCategory::Weapons => {
push_unique(&mut next_vlocker.weapons, &item_seed.classname);
push_unique_str(&mut va_categories_changed, "weapons");
}
EquipmentCategory::Magazines => {
push_unique(&mut next_vlocker.magazines, &item_seed.classname);
push_unique_str(&mut va_categories_changed, "magazines");
}
EquipmentCategory::Backpacks => {
push_unique(&mut next_vlocker.backpacks, &item_seed.classname);
push_unique_str(&mut va_categories_changed, "backpacks");
}
}
}
if next_locker.items.len() > 25 {
return Err(
"Locker capacity would exceed 25 unique items. Clear space before checkout."
.to_string(),
);
}
for category in va_categories_changed {
match category {
"items" => {
va_patch.insert(category.to_string(), json!(next_vlocker.items));
}
"weapons" => {
va_patch.insert(category.to_string(), json!(next_vlocker.weapons));
}
"magazines" => {
va_patch.insert(category.to_string(), json!(next_vlocker.magazines));
}
"backpacks" => {
va_patch.insert(category.to_string(), json!(next_vlocker.backpacks));
}
_ => {}
}
}
for vehicle_seed in &context.vehicles {
if vehicle_seed.classname.trim().is_empty() {
return Err("Vehicle checkout entry was missing a classname.".to_string());
}
let vehicle_category = resolve_vehicle_category(&vehicle_seed.category)?;
match vehicle_category {
VehicleCategory::Cars => {
push_unique(&mut next_vgarage.cars, &vehicle_seed.classname);
push_unique_str(&mut vgarage_categories_changed, "cars");
}
VehicleCategory::Armor => {
push_unique(&mut next_vgarage.armor, &vehicle_seed.classname);
push_unique_str(&mut vgarage_categories_changed, "armor");
}
VehicleCategory::Helis => {
push_unique(&mut next_vgarage.helis, &vehicle_seed.classname);
push_unique_str(&mut vgarage_categories_changed, "helis");
}
VehicleCategory::Planes => {
push_unique(&mut next_vgarage.planes, &vehicle_seed.classname);
push_unique_str(&mut vgarage_categories_changed, "planes");
}
VehicleCategory::Naval => {
push_unique(&mut next_vgarage.naval, &vehicle_seed.classname);
push_unique_str(&mut vgarage_categories_changed, "naval");
}
VehicleCategory::Other => {
push_unique(&mut next_vgarage.other, &vehicle_seed.classname);
push_unique_str(&mut vgarage_categories_changed, "other");
}
}
vehicle_granted.push(StoreGrantedVehicle {
classname: vehicle_seed.classname.clone(),
category: vehicle_seed.category.clone(),
});
}
for category in vgarage_categories_changed {
match category {
"cars" => {
vgarage_patch.insert(category.to_string(), json!(next_vgarage.cars));
}
"armor" => {
vgarage_patch.insert(category.to_string(), json!(next_vgarage.armor));
}
"helis" => {
vgarage_patch.insert(category.to_string(), json!(next_vgarage.helis));
}
"planes" => {
vgarage_patch.insert(category.to_string(), json!(next_vgarage.planes));
}
"naval" => {
vgarage_patch.insert(category.to_string(), json!(next_vgarage.naval));
}
"other" => {
vgarage_patch.insert(category.to_string(), json!(next_vgarage.other));
}
_ => {}
}
}
let mut bank_patch = HashMap::new();
let mut final_bank = None;
let mut original_bank = None;
let mut org_patch = HashMap::new();
let mut org_target_uids = Vec::new();
let mut final_org = None;
let mut original_org = None;
match payment_method.as_str() {
"cash" | "bank" => {
original_bank = Some(self.bank.get_bank(requester_uid)?);
let preview = self.bank.preview_checkout(
requester_uid,
charged_total,
payment_method.as_str(),
)?;
bank_patch = preview.patch.clone();
final_bank = Some(preview.account);
}
"org_funds" | "credit_line" => {
if context.org_id.trim().is_empty() {
return Err("A valid organization is required for this checkout.".to_string());
}
let mut org = self.org.get_org(&context.org_id)?;
original_org = Some(org.clone());
match payment_method.as_str() {
"org_funds" => {
if !can_manage_treasury(
&org,
requester_uid,
context.requester_is_default_org_ceo,
) {
return Err(
"Only the organization leader or CEO can charge org funds."
.to_string(),
);
}
if org.funds < charged_total {
return Err(
"Organization funds cannot cover this checkout.".to_string()
);
}
org.funds -= charged_total;
org_patch.insert("funds".to_string(), json!(org.funds));
}
"credit_line" => {
let credit_line =
org.credit_lines.get_mut(requester_uid).ok_or_else(|| {
"Assigned credit line cannot cover this checkout.".to_string()
})?;
if credit_line.amount < charged_total {
return Err(
"Assigned credit line cannot cover this checkout.".to_string()
);
}
credit_line.amount -= charged_total;
org_patch.insert("credit_lines".to_string(), json!(org.credit_lines));
}
_ => unreachable!(),
}
if payment_method == "org_funds" && !context.vehicles.is_empty() {
add_org_fleet_vehicles(&mut org, &context.vehicles);
org_patch.insert("fleet".to_string(), json!(org.fleet));
}
org_target_uids = resolve_member_uids(&org, Some(requester_uid));
final_org = Some(org);
}
_ => return Err("Selected payment source is unsupported.".to_string()),
}
let mut locker_saved = false;
let mut vlocker_saved = false;
let mut vgarage_saved = false;
let mut org_saved = false;
let commit_result = (|| -> Result<(), String> {
if !locker_patch.is_empty() {
self.locker
.override_locker(requester_uid, next_locker.items.clone())?;
locker_saved = true;
}
if !va_patch.is_empty() {
self.vlocker
.override_locker(requester_uid, next_vlocker.clone())?;
vlocker_saved = true;
}
if !vgarage_patch.is_empty() {
self.vgarage
.override_garage(requester_uid, next_vgarage.clone())?;
vgarage_saved = true;
}
if let Some(org) = final_org.clone() {
self.org.override_org(&context.org_id, org)?;
org_saved = true;
}
if let Some(bank) = final_bank.as_ref() {
self.bank.override_bank(requester_uid, bank)?;
}
Ok(())
})();
if let Err(error) = commit_result {
if org_saved {
if let Some(org) = original_org {
let org_id = org.id.clone();
let _ = self.org.override_org(&org_id, org);
}
}
if vgarage_saved {
let _ = self
.vgarage
.override_garage(requester_uid, original_vgarage);
}
if vlocker_saved {
let _ = self
.vlocker
.override_locker(requester_uid, original_vlocker);
}
if locker_saved {
let _ = self
.locker
.override_locker(requester_uid, original_locker.items);
}
if let Some(bank) = original_bank {
let _ = self.bank.override_bank(requester_uid, &bank);
}
return Err(error);
}
Ok(StoreCheckoutResult {
charged_total,
payment_method,
message: format!(
"Checkout completed. {} charged, {} locker grant(s), {} vehicle unlock(s).",
format_currency(charged_total),
locker_granted.len(),
vehicle_granted.len()
),
locker_granted,
vehicle_granted,
locker_patch,
va_patch,
vgarage_patch,
bank_patch,
org_patch,
org_target_uids,
})
}
}
fn checkout_total(context: &StoreCheckoutContext) -> f64 {
let item_total = context
.items
.iter()
.map(|entry| entry.price_value.max(0.0) * f64::from(entry.quantity))
.sum::<f64>();
let vehicle_total = context
.vehicles
.iter()
.map(|entry| entry.price_value.max(0.0))
.sum::<f64>();
(item_total + vehicle_total).floor()
}
fn resolve_locker_category(category: &str) -> Result<&'static str, String> {
match category.trim().to_ascii_lowercase().as_str() {
"item" | "attachment" => Ok("item"),
"weapon" => Ok("weapon"),
"magazine" => Ok("magazine"),
"backpack" => Ok("backpack"),
other => Err(format!("Store item category '{}' is unsupported.", other)),
}
}
fn resolve_arsenal_category(category: &str) -> Result<EquipmentCategory, String> {
match category.trim().to_ascii_lowercase().as_str() {
"item" | "attachment" => Ok(EquipmentCategory::Items),
"weapon" => Ok(EquipmentCategory::Weapons),
"magazine" => Ok(EquipmentCategory::Magazines),
"backpack" => Ok(EquipmentCategory::Backpacks),
other => Err(format!("Store item category '{}' is unsupported.", other)),
}
}
fn resolve_vehicle_category(category: &str) -> Result<VehicleCategory, String> {
match category.trim().to_ascii_lowercase().as_str() {
"cars" => Ok(VehicleCategory::Cars),
"armor" => Ok(VehicleCategory::Armor),
"helis" | "heli" => Ok(VehicleCategory::Helis),
"planes" => Ok(VehicleCategory::Planes),
"naval" => Ok(VehicleCategory::Naval),
"other" => Ok(VehicleCategory::Other),
other => Err(format!("Vehicle category '{}' is unsupported.", other)),
}
}
fn push_unique(values: &mut Vec<String>, value: &str) {
if !values.iter().any(|entry| entry == value) {
values.push(value.to_string());
}
}
fn push_unique_str<'a>(values: &mut Vec<&'a str>, value: &'a str) {
if !values.contains(&value) {
values.push(value);
}
}
fn can_manage_treasury(
org: &HotOrgRecord,
requester_uid: &str,
requester_is_default_org_ceo: bool,
) -> bool {
org.owner == requester_uid
|| ((org.id.eq_ignore_ascii_case("default") || org.owner.eq_ignore_ascii_case("server"))
&& requester_is_default_org_ceo)
}
fn resolve_member_uids(org: &HotOrgRecord, requester_uid: Option<&str>) -> Vec<String> {
let mut member_uids = org.members.keys().cloned().collect::<Vec<_>>();
if let Some(uid) = requester_uid {
if !uid.is_empty() && !member_uids.iter().any(|member_uid| member_uid == uid) {
member_uids.push(uid.to_string());
}
}
member_uids
}
fn add_org_fleet_vehicles(
org: &mut HotOrgRecord,
vehicles: &[forge_models::StoreCheckoutVehicleSeed],
) {
let mut fleet_index = org.fleet.len();
for vehicle in vehicles {
if vehicle.classname.trim().is_empty() {
continue;
}
let fleet_type = vehicle.category.trim().to_ascii_lowercase();
let mut fleet_key = format!("{}_{}", vehicle.classname, fleet_index);
while org.fleet.contains_key(&fleet_key) {
fleet_index += 1;
fleet_key = format!("{}_{}", vehicle.classname, fleet_index);
}
org.fleet.insert(
fleet_key,
OrgFleetEntry {
classname: vehicle.classname.clone(),
name: vehicle.classname.clone(),
fleet_type,
status: "Ready".to_string(),
damage: "0%".to_string(),
},
);
fleet_index += 1;
}
}
fn format_currency(amount: f64) -> String {
let rounded = amount.max(0.0).round() as i64;
let digits = rounded.to_string();
let mut formatted = String::new();
for (index, character) in digits.chars().rev().enumerate() {
if index > 0 && index % 3 == 0 {
formatted.push(',');
}
formatted.push(character);
}
format!("${}", formatted.chars().rev().collect::<String>())
}