diff --git a/arma/client/addons/org/XEH_postInitClient.sqf b/arma/client/addons/org/XEH_postInitClient.sqf
index 13e220b..2e9b559 100644
--- a/arma/client/addons/org/XEH_postInitClient.sqf
+++ b/arma/client/addons/org/XEH_postInitClient.sqf
@@ -11,12 +11,14 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initOrgUIBridge); };
params [["_data", createHashMap, [createHashMap]]];
GVAR(OrgClass) call ["sync", [_data, true]];
+ GVAR(OrgUIBridge) call ["refreshPortal", []];
}] call CFUNC(addEventHandler);
[QGVAR(responseSyncOrg), {
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
GVAR(OrgClass) call ["sync", [_data, _jip]];
+ GVAR(OrgUIBridge) call ["refreshPortal", []];
}] call CFUNC(addEventHandler);
[QGVAR(responseCreateOrg), {
@@ -37,6 +39,12 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initOrgUIBridge); };
GVAR(OrgUIBridge) call ["handleLeaveResponse", [_payload]];
}] call CFUNC(addEventHandler);
+[QGVAR(responseCreditLine), {
+ params [["_payload", createHashMap, [createHashMap]]];
+
+ GVAR(OrgUIBridge) call ["handleCreditLineResponse", [_payload]];
+}] call CFUNC(addEventHandler);
+
[{
EGVAR(actor,ActorClass) get "isLoaded";
}, {
diff --git a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf
index c6d8ef2..a9657c3 100644
--- a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf
@@ -38,6 +38,9 @@ switch (_event) do {
case "org::leave::request": {
GVAR(OrgUIBridge) call ["requestLeave", []];
};
+ case "org::credit::request": {
+ GVAR(OrgUIBridge) call ["requestCreditLine", [_data]];
+ };
case "org::ready": {
GVAR(OrgUIBridge) call ["handleReady", [_control]];
};
diff --git a/arma/client/addons/org/functions/fnc_initOrgClass.sqf b/arma/client/addons/org/functions/fnc_initOrgClass.sqf
index 1a5f6c5..b4ee8d5 100644
--- a/arma/client/addons/org/functions/fnc_initOrgClass.sqf
+++ b/arma/client/addons/org/functions/fnc_initOrgClass.sqf
@@ -35,6 +35,7 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
_org set ["name", ""];
_org set ["funds", 0];
_org set ["reputation", 0];
+ _org set ["credit_lines", createHashMap];
_org set ["assets", createHashMap];
_org set ["fleet", createHashMap];
_org set ["members", createHashMap];
@@ -78,6 +79,7 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
private _ownerUid = _orgData get "owner";
private _funds = _orgData get "funds";
private _reputation = _orgData get "reputation";
+ private _creditLinesRaw = _orgData getOrDefault ["credit_lines", createHashMap];
private _assetsRaw = _orgData get "assets";
private _fleetRaw = _orgData get "fleet";
private _membersRaw = _orgData get "members";
@@ -132,6 +134,16 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
]);
} 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],
@@ -149,6 +161,7 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
]],
["funds", _funds],
["reputation", _reputation],
+ ["creditLines", _creditLinesList],
["members", _membersList],
["fleet", _fleetList],
["assets", _assetsList],
diff --git a/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf b/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf
index d0c4fc5..36f7184 100644
--- a/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf
+++ b/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf
@@ -4,7 +4,7 @@
* File: fnc_initOrgUIBridge.sqf
* Author: IDSolutions
* Date: 2026-03-10
- * Last Update: 2026-03-10
+ * Last Update: 2026-03-12
* Public: No
*
* Description:
@@ -150,12 +150,37 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
_self call ["sendBridgeEvent", [_eventName, _payload]];
}],
+ ["handleCreditLineResponse", compileFinal {
+ params [["_payload", createHashMap, [createHashMap]]];
+
+ private _eventName = [
+ "org::credit::failure",
+ "org::credit::success"
+ ] select (_payload getOrDefault ["success", false]);
+
+ _self call ["sendBridgeEvent", [_eventName, _payload]];
+ }],
["requestDisband", compileFinal {
[SRPC(org,requestDisbandOrg), [getPlayerUID player]] call CFUNC(serverEvent);
}],
["requestLeave", compileFinal {
[SRPC(org,requestLeaveOrg), [getPlayerUID player]] call CFUNC(serverEvent);
}],
+ ["requestCreditLine", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _memberUid = _data getOrDefault ["memberUid", ""];
+ private _memberName = _data getOrDefault ["memberName", ""];
+ private _amount = _data getOrDefault ["amount", 0];
+
+ [SRPC(org,requestAssignCreditLine), [getPlayerUID player, _memberUid, _memberName, _amount]] call CFUNC(serverEvent);
+ }],
+ ["refreshPortal", compileFinal {
+ private _control = _self call ["getActiveBrowserControl", []];
+ if (isNull _control) exitWith { false };
+
+ _self call ["sendBridgeEvent", ["org::sync", GVAR(OrgClass) call ["buildPortalPayload", []], _control]]
+ }],
["handleReady", compileFinal {
params [["_control", controlNull, [controlNull]]];
diff --git a/arma/client/addons/org/ui/_site/bridge.js b/arma/client/addons/org/ui/_site/bridge.js
index ecc7062..94626c6 100644
--- a/arma/client/addons/org/ui/_site/bridge.js
+++ b/arma/client/addons/org/ui/_site/bridge.js
@@ -71,6 +71,23 @@
}
}
+ function requestCreditLine(payload) {
+ const sent = sendEvent("org::credit::request", payload);
+ if (sent) {
+ return true;
+ }
+
+ const OrgPortal = window.OrgPortal;
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "error",
+ "Arma credit line bridge is unavailable.",
+ );
+ }
+
+ return false;
+ }
+
function receive(eventOrPayload, data = {}) {
const event =
typeof eventOrPayload === "object" && eventOrPayload !== null
@@ -103,7 +120,38 @@
return;
}
+ if (event === "org::sync") {
+ if (store && typeof store.hydratePortal === "function") {
+ store.hydratePortal(payloadData);
+ }
+ return;
+ }
+
const OrgPortal = window.OrgPortal;
+ if (event === "org::credit::success") {
+ if (OrgPortal && OrgPortal.store) {
+ OrgPortal.store.setModal(null);
+ }
+
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "success",
+ payloadData.message || "Credit line assigned.",
+ );
+ }
+ return;
+ }
+
+ if (event === "org::credit::failure") {
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "error",
+ payloadData.message || "Unable to assign credit line.",
+ );
+ }
+ return;
+ }
+
if (event === "org::disband::success") {
if (OrgPortal && OrgPortal.store) {
OrgPortal.store.setModal(null);
@@ -170,6 +218,7 @@
requestCreateOrg,
requestDisbandOrg,
requestLeaveOrg,
+ requestCreditLine,
receive,
sendEvent,
};
@@ -179,6 +228,7 @@
requestCreateOrg,
requestDisbandOrg,
requestLeaveOrg,
+ requestCreditLine,
receive,
receiveLoginSuccess: (data) => receive("org::login::success", data),
receiveLoginFailure: (data) => receive("org::login::failure", data),
diff --git a/arma/client/addons/org/ui/_site/components/portal/modalLayer.js b/arma/client/addons/org/ui/_site/components/portal/modalLayer.js
index 611ecfb..db83264 100644
--- a/arma/client/addons/org/ui/_site/components/portal/modalLayer.js
+++ b/arma/client/addons/org/ui/_site/components/portal/modalLayer.js
@@ -156,7 +156,7 @@
"select",
{ id: "treasury-credit-member", ...memberSelectProps },
...members.map((member) =>
- h("option", { value: member.name }, member.name),
+ h("option", { value: member.uid }, member.name),
),
),
),
diff --git a/arma/client/addons/org/ui/_site/logic/portalActions.js b/arma/client/addons/org/ui/_site/logic/portalActions.js
index 81e1d72..a9b23ee 100644
--- a/arma/client/addons/org/ui/_site/logic/portalActions.js
+++ b/arma/client/addons/org/ui/_site/logic/portalActions.js
@@ -103,7 +103,11 @@
),
);
store.setCreditLines((currentLines) =>
- currentLines.filter((line) => line.member !== memberName),
+ currentLines.filter((line) =>
+ memberUid
+ ? line.uid !== memberUid
+ : line.member !== memberName,
+ ),
);
return true;
}
@@ -240,7 +244,7 @@
return true;
}
- grantCreditLine(memberName, amount) {
+ grantCreditLine(memberUid, amount) {
if (!getters.canManageTreasury()) {
this.showTreasuryNotice(
"error",
@@ -249,7 +253,7 @@
return false;
}
- if (!memberName) {
+ if (!memberUid) {
this.showTreasuryNotice(
"error",
"Select a member for the credit line.",
@@ -265,30 +269,41 @@
return false;
}
- store.setCreditLines((currentLines) => {
- const existingIndex = currentLines.findIndex(
- (line) => line.member === memberName,
+ const member = store
+ .getMembers()
+ .find(
+ (entry) =>
+ getters.getMemberUid(entry) === memberUid,
);
- if (existingIndex === -1) {
- return [
- ...currentLines,
- { member: memberName, amount },
- ];
- }
+ const memberName = member
+ ? getters.getMemberName(member)
+ : "";
- const updatedLines = [...currentLines];
- updatedLines[existingIndex] = {
- member: memberName,
- amount,
- };
- return updatedLines;
+ if (!memberName) {
+ this.showTreasuryNotice(
+ "error",
+ "Selected member was not found in the organization roster.",
+ );
+ return false;
+ }
+
+ const bridge = window.RegistryApp
+ ? window.RegistryApp.bridge
+ : null;
+
+ if (!bridge || typeof bridge.requestCreditLine !== "function") {
+ this.showTreasuryNotice(
+ "error",
+ "Credit line bridge is unavailable.",
+ );
+ return false;
+ }
+
+ return bridge.requestCreditLine({
+ memberUid,
+ memberName,
+ amount,
});
-
- this.showTreasuryNotice(
- "success",
- `Credit line of ${getters.formatCurrency(amount)} assigned to ${memberName}.`,
- );
- return true;
}
}
diff --git a/arma/client/addons/org/ui/_site/logic/portalStore.js b/arma/client/addons/org/ui/_site/logic/portalStore.js
index 99b8a19..2d52184 100644
--- a/arma/client/addons/org/ui/_site/logic/portalStore.js
+++ b/arma/client/addons/org/ui/_site/logic/portalStore.js
@@ -11,7 +11,9 @@
[this.getMembers, this.setMembers] = createSignal([
...portalData.members,
]);
- [this.getCreditLines, this.setCreditLines] = createSignal([]);
+ [this.getCreditLines, this.setCreditLines] = createSignal([
+ ...portalData.creditLines,
+ ]);
[this.getTreasuryNotice, this.setTreasuryNotice] = createSignal(
{
type: "",
@@ -26,7 +28,7 @@
hydrateFromPayload(payload) {
this.setFunds(payload.portalData.funds || 0);
this.setMembers([...(payload.portalData.members || [])]);
- this.setCreditLines([]);
+ this.setCreditLines([...(payload.portalData.creditLines || [])]);
this.setTreasuryNotice({ type: "", text: "" });
this.setModal(null);
this.setOrgDisbanded(false);
diff --git a/arma/client/addons/org/ui/_site/portal/data.js b/arma/client/addons/org/ui/_site/portal/data.js
index b3a57d1..72b4c3b 100644
--- a/arma/client/addons/org/ui/_site/portal/data.js
+++ b/arma/client/addons/org/ui/_site/portal/data.js
@@ -33,6 +33,7 @@
),
funds: 0,
reputation: 0,
+ creditLines: [],
members: [],
fleet: [],
assets: [],
@@ -77,6 +78,10 @@
);
this.portalData.funds = payload.portalData.funds || 0;
this.portalData.reputation = payload.portalData.reputation || 0;
+ replaceArray(
+ this.portalData.creditLines,
+ payload.portalData.creditLines || [],
+ );
replaceArray(
this.portalData.members,
diff --git a/arma/client/addons/store/functions/fnc_initStoreClass.sqf b/arma/client/addons/store/functions/fnc_initStoreClass.sqf
index 8d06a3e..0c1f781 100644
--- a/arma/client/addons/store/functions/fnc_initStoreClass.sqf
+++ b/arma/client/addons/store/functions/fnc_initStoreClass.sqf
@@ -4,7 +4,7 @@
* File: fnc_initStoreClass.sqf
* Author: IDSolutions
* Date: 2026-01-28
- * Last Update: 2026-03-11
+ * Last Update: 2026-03-12
* Public: Yes
*
* Description:
@@ -28,6 +28,7 @@ GVAR(StoreBaseClass) = compileFinal createHashMapFromArray [
_self set ["store", createHashMap];
_self set ["workspace", createHashMapFromArray [
["budget", 48000],
+ ["creditLine", 0],
["availability", "Open"],
["approval", "Field Access"],
["moduleState", "Preview"],
@@ -41,19 +42,106 @@ GVAR(StoreBaseClass) = compileFinal createHashMapFromArray [
}],
["buildUIPayload", compileFinal {
private _workspace = _self getOrDefault ["workspace", createHashMap];
+ private _budget = _workspace getOrDefault ["budget", 48000];
+ private _creditLine = _workspace getOrDefault ["creditLine", 0];
+ private _cashBalance = 0;
+ private _bankBalance = 0;
+ private _orgFunds = 0;
+ private _orgId = "";
+ private _orgName = "";
+ private _orgOwnerUid = "";
+ private _orgCreditLines = createHashMap;
+ private _playerUid = getPlayerUID player;
+ private _playerVar = toLowerANSI (vehicleVarName player);
+ private _isOrgLeader = false;
+ private _isDefaultOrg = false;
+ private _isDefaultOrgCeo = false;
+
+ if !(isNil QEGVAR(bank,BankClass)) then {
+ _cashBalance = EGVAR(bank,BankClass) call ["get", ["cash", 0]];
+ _bankBalance = EGVAR(bank,BankClass) call ["get", ["bank", 0]];
+ };
+
+ if !(isNil QEGVAR(org,OrgClass)) then {
+ _orgId = EGVAR(org,OrgClass) call ["get", ["id", ""]];
+ _orgName = EGVAR(org,OrgClass) call ["get", ["name", ""]];
+ _orgOwnerUid = EGVAR(org,OrgClass) call ["get", ["owner", ""]];
+ _orgFunds = EGVAR(org,OrgClass) call ["get", ["funds", 0]];
+ _orgCreditLines = EGVAR(org,OrgClass) call ["get", ["credit_lines", createHashMap]];
+ _isDefaultOrg = (_orgId isEqualTo "default") || { toLowerANSI _orgOwnerUid isEqualTo "server" };
+ _isOrgLeader = _orgOwnerUid isEqualTo _playerUid;
+ _isDefaultOrgCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" };
+ };
+
+ if (_orgCreditLines isEqualType createHashMap) then {
+ private _playerCreditLine = _orgCreditLines getOrDefault [_playerUid, createHashMap];
+ if (_playerCreditLine isEqualType createHashMap) then {
+ _creditLine = _playerCreditLine getOrDefault ["amount", _creditLine];
+ };
+ };
+
+ private _canUseOrgFunds = _isOrgLeader || _isDefaultOrgCeo;
+ private _orgFundsEnabled = _canUseOrgFunds && {_orgFunds > 0};
+ private _paymentSources = [
+ createHashMapFromArray [
+ ["id", "cash"],
+ ["label", "Cash"],
+ ["balance", _cashBalance],
+ ["enabled", _cashBalance > 0],
+ ["detail", "Use on-hand cash carried by the player."]
+ ],
+ createHashMapFromArray [
+ ["id", "bank"],
+ ["label", "Bank"],
+ ["balance", _bankBalance],
+ ["enabled", _bankBalance > 0],
+ ["detail", "Charge the player bank account."]
+ ],
+ createHashMapFromArray [
+ ["id", "org_funds"],
+ ["label", "Org Funds"],
+ ["balance", _orgFunds],
+ ["enabled", _orgFundsEnabled],
+ ["detail", [
+ "Only organization leaders or the default-org CEO can use treasury funds.",
+ [
+ "Charge organization treasury funds.",
+ "No organization funds are currently available."
+ ] select _orgFundsEnabled
+ ] select _canUseOrgFunds]
+ ],
+ createHashMapFromArray [
+ ["id", "credit_line"],
+ ["label", "Credit Line"],
+ ["balance", _creditLine],
+ ["enabled", _creditLine > 0],
+ ["detail", [
+ "No approved credit line is assigned to this member.",
+ "Use the approved procurement credit line."
+ ] select (_creditLine > 0)]
+ ]
+ ];
createHashMapFromArray [
["session", createHashMapFromArray [
["actorName", name player],
["actorUid", getPlayerUID player],
- ["approvalRole", _workspace getOrDefault ["approval", "Field Access"]]
+ ["approvalRole", _workspace getOrDefault ["approval", "Field Access"]],
+ ["orgId", _orgId],
+ ["orgName", _orgName],
+ ["orgLeader", _isOrgLeader],
+ ["defaultOrgCeo", _isDefaultOrgCeo],
+ ["canUseOrgFunds", _canUseOrgFunds]
]],
["workspace", createHashMapFromArray [
- ["budget", _workspace getOrDefault ["budget", 48000]],
+ ["budget", _budget],
+ ["creditLine", _creditLine],
["availability", _workspace getOrDefault ["availability", "Open"]],
["approval", _workspace getOrDefault ["approval", "Field Access"]],
["moduleState", _workspace getOrDefault ["moduleState", "Preview"]],
- ["searchTags", _workspace getOrDefault ["searchTags", ["Field", "Logistics", "Issued", "Restricted"]]]
+ ["searchTags", _workspace getOrDefault ["searchTags", ["Field", "Logistics", "Issued", "Restricted"]]],
+ ["paymentSources", _paymentSources],
+ ["defaultPaymentSource", "cash"]
]],
["cartItems", []]
]
diff --git a/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf b/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf
index 3bb1a44..d5d99d4 100644
--- a/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf
+++ b/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf
@@ -4,7 +4,7 @@
* File: fnc_initStoreUIBridge.sqf
* Author: IDSolutions
* Date: 2026-03-10
- * Last Update: 2026-03-11
+ * Last Update: 2026-03-12
* Public: No
*
* Description:
@@ -302,7 +302,8 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
params [["_data", createHashMap, [createHashMap]]];
private _items = _data getOrDefault ["items", []];
- private _message = format ["Checkout integration is not wired yet. Received %1 queued line(s).", count _items];
+ private _paymentMethod = _data getOrDefault ["paymentMethod", "cash"];
+ private _message = format ["Checkout integration is not wired yet. Received %1 queued line(s) using %2.", count _items, _paymentMethod];
diag_log format ["[FORGE:Client:Store] Checkout request received: %1", _data];
_self call ["sendBridgeEvent", ["store::checkout::failure", createHashMapFromArray [["message", _message]]]];
diff --git a/arma/client/addons/store/ui/_site/components/AppShell.js b/arma/client/addons/store/ui/_site/components/AppShell.js
index ba997a4..70c2f27 100644
--- a/arma/client/addons/store/ui/_site/components/AppShell.js
+++ b/arma/client/addons/store/ui/_site/components/AppShell.js
@@ -491,6 +491,11 @@ ${scopeSelector} .store-toast.is-error {
getters.getSelectionKey(state) || "Catalog",
)
: actions.formatTitle(state.view);
+ const selectedPaymentSource =
+ getters.getPaymentSourceById(
+ storeConfig,
+ state.selectedPaymentSource,
+ ) || null;
ensureScopedStyle("storefront-app-shell", appShellCss);
@@ -687,6 +692,27 @@ ${scopeSelector} .store-toast.is-error {
),
),
),
+ h(
+ "div",
+ { className: "filter-group" },
+ h(
+ "span",
+ { className: "filter-label" },
+ "Payment",
+ ),
+ h(
+ "div",
+ { className: "filter-value" },
+ h("span", null, "Checkout Source"),
+ h(
+ "span",
+ { className: "filter-placeholder" },
+ selectedPaymentSource
+ ? selectedPaymentSource.label
+ : "Cash",
+ ),
+ ),
+ ),
),
),
),
diff --git a/arma/client/addons/store/ui/_site/components/cart.js b/arma/client/addons/store/ui/_site/components/cart.js
index 68f6ff8..57be6ad 100644
--- a/arma/client/addons/store/ui/_site/components/cart.js
+++ b/arma/client/addons/store/ui/_site/components/cart.js
@@ -55,8 +55,39 @@ ${scopeSelector} .cart-header {
}
${scopeSelector} .cart-close {
- min-width: 2rem;
- height: 2rem;
+ min-width: 2.1rem;
+ height: 2.1rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border-radius: 0.6rem;
+ border: 1px solid rgb(173 48 48 / 0.9);
+ background: linear-gradient(
+ 180deg,
+ rgb(214 92 92) 0%,
+ rgb(175 52 52) 100%
+ );
+ color: #fff;
+ font-size: 0.92rem;
+ font-weight: 800;
+ line-height: 1;
+ box-shadow:
+ inset 0 1px 0 rgb(255 255 255 / 0.26),
+ 0 8px 18px rgb(138 61 61 / 0.28);
+}
+
+${scopeSelector} .cart-close:hover {
+ background: linear-gradient(
+ 180deg,
+ rgb(226 107 107) 0%,
+ rgb(187 61 61) 100%
+ );
+ border-color: rgb(173 48 48);
+}
+
+${scopeSelector} .cart-close:focus-visible {
+ outline: 2px solid rgb(191 80 80 / 0.35);
}
${scopeSelector} .cart-status,
@@ -175,6 +206,60 @@ ${scopeSelector} .cart-summary {
gap: 0.7rem;
}
+${scopeSelector} .payment-source-field {
+ display: grid;
+ gap: 0.65rem;
+}
+
+${scopeSelector} .payment-source-select {
+ width: 100%;
+ min-height: 2.9rem;
+ padding: 0 0.95rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--store-border);
+ background: rgb(255 255 255 / 0.78);
+ color: var(--store-text-main);
+}
+
+${scopeSelector} .payment-source-meta,
+${scopeSelector} .payment-source-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+}
+
+${scopeSelector} .payment-source-meta {
+ padding: 0.85rem 0.9rem;
+ border-radius: 0.95rem;
+ border: 1px solid var(--store-border);
+ background: rgb(255 255 255 / 0.44);
+}
+
+${scopeSelector} .payment-source-detail {
+ margin: 0.2rem 0 0;
+ font-size: 0.82rem;
+ line-height: 1.4;
+ color: var(--store-text-muted);
+}
+
+${scopeSelector} .payment-source-label {
+ font-weight: 700;
+ color: var(--store-text-main);
+}
+
+${scopeSelector} .payment-source-balance {
+ font-weight: 700;
+ color: var(--store-success);
+}
+
+${scopeSelector} .payment-source-state {
+ font-size: 0.7rem;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--store-text-subtle);
+}
+
${scopeSelector} .summary-row.total {
font-size: 1rem;
font-weight: 700;
@@ -217,9 +302,24 @@ ${scopeSelector} .cart-empty {
StorefrontApp.componentFns.Cart = function Cart() {
const state = getters.getStoreState(store);
const summary = getters.summarizeCart(state.cartItems);
- const remainingBudget = Math.max(
+ const paymentSources = getters.getPaymentSources(storeConfig);
+ const selectedPaymentSource =
+ getters.getPaymentSourceById(
+ storeConfig,
+ state.selectedPaymentSource,
+ ) || paymentSources[0] || null;
+ const availablePaymentSourceCount = paymentSources.filter(
+ (source) => source.enabled !== false,
+ ).length;
+ const selectedPaymentLabel = selectedPaymentSource
+ ? selectedPaymentSource.label
+ : "Unavailable";
+ const selectedPaymentBalance = selectedPaymentSource
+ ? Number(selectedPaymentSource.balance || 0)
+ : 0;
+ const remainingSourceBalance = Math.max(
0,
- Number(storeConfig.budget || 0) - summary.total,
+ selectedPaymentBalance - summary.total,
);
ensureScopedStyle("storefront-cart", cartCss);
@@ -254,8 +354,7 @@ ${scopeSelector} .cart-empty {
"button",
{
type: "button",
- className:
- "window-control-btn cart-close is-close",
+ className: "cart-close",
"aria-label": "Close cart",
title: "Close cart",
onClick: () => actions.closeCart(),
@@ -263,25 +362,13 @@ ${scopeSelector} .cart-empty {
"X",
),
),
- h(
- "div",
- { className: "cart-status" },
- h("span", { className: "eyebrow" }, "Status"),
- h(
- "p",
- { className: "section-copy" },
- state.isCheckingOut
- ? "Checkout request sent through the browser bridge."
- : "Local cart state is active. Checkout is routed through the same bridge contract used by the org UI.",
- ),
- ),
h(
"div",
{ className: "cart-kpi" },
h(
"div",
{ className: "cart-kpi-card" },
- h("span", { className: "kpi-label" }, "Lines"),
+ h("span", { className: "kpi-label" }, "Items"),
h(
"span",
{ className: "kpi-value" },
@@ -291,14 +378,114 @@ ${scopeSelector} .cart-empty {
h(
"div",
{ className: "cart-kpi-card" },
- h("span", { className: "kpi-label" }, "Budget"),
+ h(
+ "span",
+ { className: "kpi-label" },
+ "Payment",
+ ),
h(
"span",
{ className: "kpi-value" },
- getters.formatCurrency(storeConfig.budget),
+ selectedPaymentLabel,
),
),
),
+ h(
+ "div",
+ { className: "cart-status" },
+ h(
+ "span",
+ { className: "eyebrow" },
+ "Payment Source",
+ ),
+ h(
+ "div",
+ { className: "payment-source-field" },
+ h(
+ "select",
+ {
+ className: "payment-source-select",
+ value: state.selectedPaymentSource,
+ onChange: (event) =>
+ actions.selectPaymentSource(
+ event.target.value,
+ ),
+ },
+ paymentSources.map((source) =>
+ h(
+ "option",
+ {
+ value: source.id,
+ disabled:
+ source.enabled === false,
+ },
+ source.enabled === false
+ ? `${source.label} (Locked)`
+ : source.label,
+ ),
+ ),
+ ),
+ selectedPaymentSource
+ ? h(
+ "div",
+ {
+ className:
+ "payment-source-meta",
+ },
+ h(
+ "div",
+ null,
+ h(
+ "div",
+ {
+ className:
+ "payment-source-row",
+ },
+ h(
+ "span",
+ {
+ className:
+ "payment-source-label",
+ },
+ selectedPaymentSource.label,
+ ),
+ h(
+ "span",
+ {
+ className:
+ "payment-source-balance",
+ },
+ getters.formatCurrency(
+ selectedPaymentSource.balance,
+ ),
+ ),
+ ),
+ h(
+ "p",
+ {
+ className:
+ "payment-source-detail",
+ },
+ selectedPaymentSource.detail,
+ ),
+ ),
+ h(
+ "span",
+ {
+ className:
+ "payment-source-state",
+ },
+ availablePaymentSourceCount > 0
+ ? selectedPaymentSource.enabled ===
+ false
+ ? "Locked"
+ : "Available"
+ : "Unavailable",
+ ),
+ )
+ : null,
+ ),
+ ),
h(
"div",
{
@@ -436,12 +623,14 @@ ${scopeSelector} .cart-empty {
h(
"span",
{ className: "summary-label" },
- "Remaining Budget",
+ "Remaining Source",
),
h(
"span",
{ className: "summary-value" },
- getters.formatCurrency(remainingBudget),
+ getters.formatCurrency(
+ remainingSourceBalance,
+ ),
),
),
h(
diff --git a/arma/client/addons/store/ui/_site/data.js b/arma/client/addons/store/ui/_site/data.js
index 15639c3..6f191e2 100644
--- a/arma/client/addons/store/ui/_site/data.js
+++ b/arma/client/addons/store/ui/_site/data.js
@@ -5,14 +5,51 @@
actorName: "",
actorUid: "",
approvalRole: "Field Access",
+ orgId: "",
+ orgName: "",
+ orgLeader: false,
+ defaultOrgCeo: false,
+ canUseOrgFunds: false,
};
const defaultStoreConfig = {
budget: 48000,
+ creditLine: 0,
availability: "Open",
approval: "Field Access",
moduleState: "Preview",
searchTags: ["Field", "Logistics", "Issued", "Restricted"],
+ paymentSources: [
+ {
+ id: "cash",
+ label: "Cash",
+ balance: 0,
+ enabled: false,
+ detail: "Use on-hand cash carried by the player.",
+ },
+ {
+ id: "bank",
+ label: "Bank",
+ balance: 0,
+ enabled: false,
+ detail: "Charge the player bank account.",
+ },
+ {
+ id: "org_funds",
+ label: "Org Funds",
+ balance: 0,
+ enabled: false,
+ detail: "Only organization leaders or the default-org CEO can use treasury funds.",
+ },
+ {
+ id: "credit_line",
+ label: "Credit Line",
+ balance: 0,
+ enabled: false,
+ detail: "No approved credit line is assigned to this member.",
+ },
+ ],
+ defaultPaymentSource: "cash",
};
function cloneValue(value) {
diff --git a/arma/client/addons/store/ui/_site/logic/events.js b/arma/client/addons/store/ui/_site/logic/events.js
index 3ae085b..a9203d4 100644
--- a/arma/client/addons/store/ui/_site/logic/events.js
+++ b/arma/client/addons/store/ui/_site/logic/events.js
@@ -165,7 +165,6 @@
return nextItems;
});
- store.setCartOpen(true);
showNotice("success", `${item.name} added to the acquisition queue.`);
}
@@ -199,6 +198,31 @@
);
}
+ function selectPaymentSource(paymentSourceId) {
+ const sourceId = String(paymentSourceId || "").trim();
+ const paymentSources = getters.getPaymentSources(storeConfig);
+ const selectedSource = paymentSources.find(
+ (source) => source.id === sourceId,
+ );
+
+ if (!selectedSource) {
+ showNotice("error", "Selected payment source is unavailable.");
+ return false;
+ }
+
+ if (selectedSource.enabled === false) {
+ showNotice(
+ "error",
+ selectedSource.detail ||
+ "Selected payment source is not available.",
+ );
+ return false;
+ }
+
+ store.setSelectedPaymentSource(sourceId);
+ return true;
+ }
+
function requestCheckout() {
const cartItems = store.getCartItems();
if (cartItems.length === 0) {
@@ -207,10 +231,29 @@
}
const summary = getters.summarizeCart(cartItems);
- if (summary.total > Number(storeConfig.budget || 0)) {
+ const selectedPaymentSource = getters.getPaymentSourceById(
+ storeConfig,
+ store.getSelectedPaymentSource(),
+ );
+
+ if (!selectedPaymentSource) {
+ showNotice("error", "Select a payment source before checkout.");
+ return false;
+ }
+
+ if (selectedPaymentSource.enabled === false) {
showNotice(
"error",
- "Checkout total exceeds the current procurement budget.",
+ selectedPaymentSource.detail ||
+ "Selected payment source is unavailable.",
+ );
+ return false;
+ }
+
+ if (summary.total > Number(selectedPaymentSource.balance || 0)) {
+ showNotice(
+ "error",
+ `${selectedPaymentSource.label} cannot cover this checkout total.`,
);
return false;
}
@@ -226,6 +269,8 @@
const sent = bridge.requestCheckout({
actorUid: session.actorUid,
actorName: session.actorName,
+ paymentMethod: selectedPaymentSource.id,
+ paymentLabel: selectedPaymentSource.label,
items: cartItems,
subtotal: summary.subtotal,
total: summary.total,
@@ -257,6 +302,7 @@
incrementCartItem,
decrementCartItem,
removeCartItem,
+ selectPaymentSource,
requestCheckout,
formatTitle: getters.formatTitle,
formatCurrency: getters.formatCurrency,
diff --git a/arma/client/addons/store/ui/_site/logic/store.js b/arma/client/addons/store/ui/_site/logic/store.js
index ab7e05f..86132a0 100644
--- a/arma/client/addons/store/ui/_site/logic/store.js
+++ b/arma/client/addons/store/ui/_site/logic/store.js
@@ -29,6 +29,8 @@
});
[this.getIsCheckingOut, this.setIsCheckingOut] =
createSignal(false);
+ [this.getSelectedPaymentSource, this.setSelectedPaymentSource] =
+ createSignal("cash");
}
resetToCategories() {
@@ -163,6 +165,58 @@
this.finishCategoryRequest(categoryKey);
}
+ ensureSelectedPaymentSource(workspace) {
+ const paymentSources = Array.isArray(workspace?.paymentSources)
+ ? workspace.paymentSources
+ : [];
+ const currentSource = String(
+ this.getSelectedPaymentSource() || "",
+ ).trim();
+ const defaultSource = String(
+ workspace?.defaultPaymentSource || "",
+ ).trim();
+ const sourceIds = paymentSources.map((source) =>
+ String(source?.id || "").trim(),
+ );
+ const enabledSource = paymentSources.find(
+ (source) => source && source.enabled !== false,
+ );
+ const defaultAvailable =
+ defaultSource && sourceIds.includes(defaultSource)
+ ? paymentSources.find(
+ (source) =>
+ String(source?.id || "").trim() ===
+ defaultSource,
+ )
+ : null;
+
+ if (
+ currentSource &&
+ sourceIds.includes(currentSource) &&
+ paymentSources.some(
+ (source) =>
+ String(source?.id || "").trim() === currentSource &&
+ source?.enabled !== false,
+ )
+ ) {
+ return;
+ }
+
+ if (defaultAvailable && defaultAvailable.enabled !== false) {
+ this.setSelectedPaymentSource(defaultSource);
+ return;
+ }
+
+ if (enabledSource) {
+ this.setSelectedPaymentSource(
+ String(enabledSource.id || "cash"),
+ );
+ return;
+ }
+
+ this.setSelectedPaymentSource(defaultSource || "cash");
+ }
+
navigateToBreadcrumb(target) {
switch (target) {
case "categories":
@@ -198,6 +252,9 @@
this.setCatalogRequestKey("");
this.setIsCatalogLoading(false);
this.setCatalogPage(1);
+ this.ensureSelectedPaymentSource(
+ payload?.workspace || payload?.storeConfig || {},
+ );
}
}
diff --git a/arma/client/addons/store/ui/_site/pages/StoreView.js b/arma/client/addons/store/ui/_site/pages/StoreView.js
index 3224d08..93f6144 100644
--- a/arma/client/addons/store/ui/_site/pages/StoreView.js
+++ b/arma/client/addons/store/ui/_site/pages/StoreView.js
@@ -1,6 +1,6 @@
(function () {
const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {});
- const CATALOG_PAGE_SIZE = 24;
+ const CATALOG_PAGE_SIZE = 6;
function getSelectionKey(state) {
return (
@@ -54,6 +54,7 @@
selectedCategory: store.getSelectedCategory(),
selectedWeaponSlot: store.getSelectedWeaponSlot(),
selectedVehicleSlot: store.getSelectedVehicleSlot(),
+ selectedPaymentSource: store.getSelectedPaymentSource(),
cartOpen: store.getCartOpen(),
searchQuery: store.getSearchQuery(),
cartItems: store.getCartItems(),
@@ -241,6 +242,27 @@
};
}
+ function getPaymentSources(storeConfig) {
+ const paymentSources = Array.isArray(storeConfig?.paymentSources)
+ ? storeConfig.paymentSources
+ : [];
+
+ return paymentSources.map((source) => ({
+ id: String(source?.id || "").trim(),
+ label: String(source?.label || source?.id || "").trim(),
+ balance: Number(source?.balance || 0),
+ enabled: source?.enabled !== false,
+ detail: String(source?.detail || "").trim(),
+ }));
+ }
+
+ function getPaymentSourceById(storeConfig, paymentSourceId) {
+ const sourceId = String(paymentSourceId || "").trim();
+ return getPaymentSources(storeConfig).find(
+ (source) => source.id === sourceId,
+ );
+ }
+
StorefrontApp.getters = {
formatTitle,
formatCurrency,
@@ -255,5 +277,7 @@
getVisibleItemsPage,
getCatalogPagination,
summarizeCart,
+ getPaymentSources,
+ getPaymentSourceById,
};
})();
diff --git a/arma/server/addons/org/XEH_preInit.sqf b/arma/server/addons/org/XEH_preInit.sqf
index 1ebf302..41fab57 100644
--- a/arma/server/addons/org/XEH_preInit.sqf
+++ b/arma/server/addons/org/XEH_preInit.sqf
@@ -69,6 +69,39 @@ PREP_RECOMPILE_END;
GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _key, _fieldValuePairs, _sync]];
}] call CFUNC(addEventHandler);
+[QGVAR(requestAssignCreditLine), {
+ params [
+ ["_uid", "", [""]],
+ ["_memberUid", "", [""]],
+ ["_memberName", "", [""]],
+ ["_amount", 0, [0]]
+ ];
+
+ if (_uid isEqualTo "" || { _memberUid isEqualTo "" } || { _amount <= 0 }) exitWith {
+ diag_log "[FORGE:Server:Org] Invalid credit line request payload!"
+ };
+
+ private _requester = [_uid] call EFUNC(common,getPlayer);
+ if (_requester isEqualTo objNull) exitWith {};
+
+ private _result = GVAR(OrgStore) call ["assignCreditLine", [_uid, _memberUid, _memberName, _amount]];
+ if (_result getOrDefault ["success", false]) then {
+ private _patch = _result getOrDefault ["patch", createHashMap];
+
+ {
+ private _memberPlayer = [_x] call EFUNC(common,getPlayer);
+ if (_memberPlayer isNotEqualTo objNull && { _patch isNotEqualTo createHashMap }) then {
+ [CRPC(org,responseSyncOrg), [_patch], _memberPlayer] call CFUNC(targetEvent);
+ };
+ } forEach (_result getOrDefault ["memberUids", []]);
+ };
+
+ [CRPC(org,responseCreditLine), [createHashMapFromArray [
+ ["success", _result getOrDefault ["success", false]],
+ ["message", _result getOrDefault ["message", "Unable to assign credit line."]]
+ ]], _requester] call CFUNC(targetEvent);
+}] call CFUNC(addEventHandler);
+
[QGVAR(requestSaveOrg), {
params [["_uid", "", [""]]];
diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf
index 83d3c83..9c9ff53 100644
--- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf
+++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf
@@ -32,6 +32,7 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
_org set ["name", ""];
_org set ["funds", 0];
_org set ["reputation", 0];
+ _org set ["credit_lines", createHashMap];
_org set ["assets", createHashMap];
_org set ["fleet", createHashMap];
_org set ["members", createHashMap];
@@ -57,13 +58,15 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
private _name = _org get "name";
private _funds = _org get "funds";
private _reputation = _org get "reputation";
+ private _creditLines = _org getOrDefault ["credit_lines", createHashMap];
- [_id, _owner, _name, _funds, _reputation] try {
+ [_id, _owner, _name, _funds, _reputation, _creditLines] try {
if (_id isEqualTo "" || !(_id isEqualType "")) then { throw "Invalid ID!"; };
if (_owner isEqualTo "" || !(_owner isEqualType "")) then { throw "Invalid Owner!"; };
if (_name isEqualTo "" || !(_name isEqualType "")) then { throw "Invalid Name!"; };
if (_funds isEqualTo 0 || !(_funds isEqualType 0)) then { throw "Invalid Funds!"; };
if (_reputation isEqualTo 0 || !(_reputation isEqualType 0)) then { throw "Invalid Reputation!"; };
+ if !(_creditLines isEqualType createHashMap) then { throw "Invalid Credit Lines!"; };
} catch {
["ERROR", format ["Failed to validate org %1!", _exception]] call EFUNC(common,log);
false
@@ -91,6 +94,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["name", "Forge Dynamics"],
["funds", 200000],
["reputation", 0],
+ ["credit_lines", createHashMap],
["assets", createHashMap],
["fleet", createHashMap],
["members", createHashMap]
@@ -110,6 +114,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_finalOrg set ["name", "Forge Dynamics"];
_finalOrg set ["funds", 200000];
_finalOrg set ["reputation", 0];
+ _finalOrg set ["credit_lines", createHashMap];
private _json = _self call ["toJSON", [_finalOrg]];
["org:create", ["default", _json]] call EFUNC(extension,extCall);
@@ -153,18 +158,15 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
private _org = _self call ["fetch", ["org:get", _orgID]];
if (_org isEqualTo createHashMap) exitWith { _org };
+ _org = GVAR(OrgModel) call ["migrate", [_org]];
private _memberRows = _self call ["fetch", ["org:members:get", _orgID]];
- if !(_memberRows isEqualType []) then {
- _memberRows = [];
- };
+ if !(_memberRows isEqualType []) then { _memberRows = []; };
private _memberMap = createHashMap;
{
private _memberUid = _x getOrDefault ["uid", ""];
- if (_memberUid isNotEqualTo "") then {
- _memberMap set [_memberUid, _x];
- };
+ if (_memberUid isNotEqualTo "") then { _memberMap set [_memberUid, _x]; };
} forEach _memberRows;
_org set ["members", _memberMap];
@@ -407,6 +409,86 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result set ["members", _memberResults];
_result
}],
+ ["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 = _self call ["loadById", [_orgID]];
+ if (_org isEqualTo createHashMap) exitWith {
+ _result set ["message", "Unable to load organization data for credit line assignment."];
+ _result
+ };
+
+ private _ownerUid = _org getOrDefault ["owner", ""];
+ private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
+ private _isDefaultOrg = (_orgID isEqualTo "default") || { toLower _ownerUid isEqualTo "server" };
+ private _isDefaultOrgCeo = _isDefaultOrg
+ && { _requesterPlayer isNotEqualTo objNull }
+ && { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" };
+ private _canManageTreasury = (_ownerUid isEqualTo _requesterUid) || _isDefaultOrgCeo;
+
+ if !_canManageTreasury 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 = _self call ["set", [GVAR(Registry), "org:update", _orgID, "credit_lines", _creditLines, true]];
+ private _memberUids = keys _members;
+ if !(_requesterUid in _memberUids) then {
+ _memberUids pushBack _requesterUid;
+ };
+
+ _result set ["success", true];
+ _result set ["message", format ["Credit line of $%1 assigned to %2.", [_amount] call BIS_fnc_numberText, _resolvedMemberName]];
+ _result set ["patch", _patch];
+ _result set ["memberUids", _memberUids];
+ _result
+ }],
["register", compileFinal {
params [["_uid", "", [""]], ["_orgName", "", [""]]];
@@ -454,6 +536,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["name", _orgName],
["funds", 0],
["reputation", 0],
+ ["credit_lines", createHashMap],
["assets", createHashMap],
["fleet", createHashMap],
["members", createHashMap]
@@ -466,9 +549,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result
};
- if (_createResult isNotEqualTo "") then {
- _org = _self call ["toHashMap", [_createResult]];
- };
+ if (_createResult isNotEqualTo "") then { _org = _self call ["toHashMap", [_createResult]]; };
_org set ["members", createHashMap];
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
diff --git a/arma/server/addons/store/$PBOPREFIX$ b/arma/server/addons/store/$PBOPREFIX$
new file mode 100644
index 0000000..ed419a2
--- /dev/null
+++ b/arma/server/addons/store/$PBOPREFIX$
@@ -0,0 +1 @@
+forge\forge_server\addons\store
diff --git a/arma/server/addons/store/CfgEventHandlers.hpp b/arma/server/addons/store/CfgEventHandlers.hpp
new file mode 100644
index 0000000..f6503c2
--- /dev/null
+++ b/arma/server/addons/store/CfgEventHandlers.hpp
@@ -0,0 +1,17 @@
+class Extended_PreStart_EventHandlers {
+ class ADDON {
+ init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
+ };
+};
+
+class Extended_PreInit_EventHandlers {
+ class ADDON {
+ init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
+ };
+};
+
+class Extended_PostInit_EventHandlers {
+ class ADDON {
+ init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
+ };
+};
diff --git a/arma/server/addons/store/README.md b/arma/server/addons/store/README.md
new file mode 100644
index 0000000..84beb40
--- /dev/null
+++ b/arma/server/addons/store/README.md
@@ -0,0 +1,3 @@
+# forge_server_store
+
+Description for this addon
diff --git a/arma/server/addons/store/XEH_PREP.hpp b/arma/server/addons/store/XEH_PREP.hpp
new file mode 100644
index 0000000..7845920
--- /dev/null
+++ b/arma/server/addons/store/XEH_PREP.hpp
@@ -0,0 +1,2 @@
+// PREP(initStore);
+// PREP(initStoreStore);
diff --git a/arma/server/addons/store/XEH_postInit.sqf b/arma/server/addons/store/XEH_postInit.sqf
new file mode 100644
index 0000000..b911595
--- /dev/null
+++ b/arma/server/addons/store/XEH_postInit.sqf
@@ -0,0 +1,3 @@
+#include "script_component.hpp"
+
+// call FUNC(initStore);
diff --git a/arma/server/addons/store/XEH_preInit.sqf b/arma/server/addons/store/XEH_preInit.sqf
new file mode 100644
index 0000000..9dd4ebf
--- /dev/null
+++ b/arma/server/addons/store/XEH_preInit.sqf
@@ -0,0 +1,7 @@
+#include "script_component.hpp"
+
+PREP_RECOMPILE_START;
+#include "XEH_PREP.hpp"
+PREP_RECOMPILE_END;
+
+// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
diff --git a/arma/server/addons/store/XEH_preStart.sqf b/arma/server/addons/store/XEH_preStart.sqf
new file mode 100644
index 0000000..a51262a
--- /dev/null
+++ b/arma/server/addons/store/XEH_preStart.sqf
@@ -0,0 +1,2 @@
+#include "script_component.hpp"
+#include "XEH_PREP.hpp"
diff --git a/arma/server/addons/store/config.cpp b/arma/server/addons/store/config.cpp
new file mode 100644
index 0000000..cbd0a75
--- /dev/null
+++ b/arma/server/addons/store/config.cpp
@@ -0,0 +1,20 @@
+#include "script_component.hpp"
+
+class CfgPatches {
+ class ADDON {
+ author = AUTHOR;
+ authors[] = {"J.Schmidt"};
+ url = ECSTRING(main,url);
+ name = COMPONENT_NAME;
+ requiredVersion = REQUIRED_VERSION;
+ requiredAddons[] = {
+ "forge_server_main",
+ "forge_server_common"
+ };
+ units[] = {};
+ weapons[] = {};
+ VERSION_CONFIG;
+ };
+};
+
+#include "CfgEventHandlers.hpp"
diff --git a/arma/server/addons/store/script_component.hpp b/arma/server/addons/store/script_component.hpp
new file mode 100644
index 0000000..0b59e38
--- /dev/null
+++ b/arma/server/addons/store/script_component.hpp
@@ -0,0 +1,9 @@
+#define COMPONENT store
+#define COMPONENT_BEAUTIFIED Store
+#include "\forge\forge_server\addons\main\script_mod.hpp"
+
+// #define DEBUG_MODE_FULL
+// #define DISABLE_COMPILE_CACHE
+// #define ENABLE_PERFORMANCE_COUNTERS
+
+#include "\forge\forge_server\addons\main\script_macros.hpp"
diff --git a/arma/server/addons/store/stringtable.xml b/arma/server/addons/store/stringtable.xml
new file mode 100644
index 0000000..464a672
--- /dev/null
+++ b/arma/server/addons/store/stringtable.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ Store
+
+
+
diff --git a/lib/models/src/lib.rs b/lib/models/src/lib.rs
index b3406aa..b710d4f 100644
--- a/lib/models/src/lib.rs
+++ b/lib/models/src/lib.rs
@@ -10,6 +10,6 @@ pub use actor::Actor;
pub use bank::Bank;
pub use garage::{Garage, HitPoints, Vehicle};
pub use locker::{Item, Locker};
-pub use org::{MemberSummary, Org};
+pub use org::{CreditLineSummary, MemberSummary, Org};
pub use v_garage::{VGarage, VehicleCategory};
pub use v_locker::{EquipmentCategory, VLocker};
diff --git a/lib/models/src/org.rs b/lib/models/src/org.rs
index a6953d6..b4da3fa 100644
--- a/lib/models/src/org.rs
+++ b/lib/models/src/org.rs
@@ -1,6 +1,14 @@
use arma_rs::{FromArma, IntoArma};
use forge_shared::OrgValidationError;
use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreditLineSummary {
+ pub uid: String,
+ pub name: String,
+ pub amount: f64,
+}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Org {
@@ -12,6 +20,8 @@ pub struct Org {
pub funds: f64,
#[serde(default)]
pub reputation: i64,
+ #[serde(default)]
+ pub credit_lines: HashMap,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -28,6 +38,7 @@ impl Org {
name: name.into(),
funds: 0.0,
reputation: 0,
+ credit_lines: HashMap::new(),
};
org.validate()?;
@@ -65,6 +76,26 @@ impl Org {
return Err(OrgValidationError::InvalidName(self.name.clone()));
}
+ for (uid, credit_line) in &self.credit_lines {
+ let resolved_uid = if credit_line.uid.trim().is_empty() {
+ uid
+ } else {
+ &credit_line.uid
+ };
+
+ if !resolved_uid.chars().all(|c| c.is_numeric()) || resolved_uid.len() != 17 {
+ return Err(OrgValidationError::InvalidCreditLineUid(
+ resolved_uid.to_string(),
+ ));
+ }
+
+ if credit_line.amount < 0.0 {
+ return Err(OrgValidationError::NegativeCreditLine(
+ resolved_uid.to_string(),
+ ));
+ }
+ }
+
Ok(())
}
diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs
index bead927..76f8b60 100644
--- a/lib/services/src/org.rs
+++ b/lib/services/src/org.rs
@@ -5,8 +5,9 @@
//!
//! For full documentation, architecture, and examples, see the [crate README](../README.md).
-use forge_models::{MemberSummary, Org};
+use forge_models::{CreditLineSummary, MemberSummary, Org};
use forge_repositories::OrgRepository;
+use std::collections::HashMap;
/// Service layer implementation for organization business logic and operations.
///
@@ -138,6 +139,21 @@ impl OrgService {
return Err("Reputation must be an integer".to_string());
}
}
+ "credit_lines" => {
+ if value.is_null() {
+ updated_org.credit_lines = HashMap::new();
+ } else {
+ updated_org.credit_lines = serde_json::from_value::<
+ HashMap,
+ >(value.clone())
+ .map_err(|e| {
+ format!(
+ "Credit lines must be an object of member credit entries: {}",
+ e
+ )
+ })?;
+ }
+ }
_ => {
return Err(format!("Unknown field: {}", field));
}
diff --git a/lib/shared/src/validation.rs b/lib/shared/src/validation.rs
index 5aa69eb..2c2152d 100644
--- a/lib/shared/src/validation.rs
+++ b/lib/shared/src/validation.rs
@@ -80,9 +80,11 @@ pub enum OrgValidationError {
EmptyOwner,
EmptyName,
NegativeFunds,
+ NegativeCreditLine(String),
InvalidId(String),
InvalidOwner(String),
InvalidName(String),
+ InvalidCreditLineUid(String),
}
impl fmt::Display for OrgValidationError {
@@ -94,6 +96,9 @@ impl fmt::Display for OrgValidationError {
OrgValidationError::NegativeFunds => {
write!(f, "Organization funds cannot be negative")
}
+ OrgValidationError::NegativeCreditLine(uid) => {
+ write!(f, "Credit line for '{}' cannot be negative", uid)
+ }
OrgValidationError::InvalidId(id) => write!(
f,
"Invalid organization ID '{}' - must contain only alphanumeric characters and underscores",
@@ -107,6 +112,11 @@ impl fmt::Display for OrgValidationError {
"Invalid organization name '{}' - cannot exceed 100 characters or contain control characters",
name
),
+ OrgValidationError::InvalidCreditLineUid(uid) => write!(
+ f,
+ "Invalid credit line UID '{}' - must be a 17-digit Steam ID",
+ uid
+ ),
}
}
}