From 6dda184d54aec427c18ac22daaa35e43db678fcf Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Thu, 12 Mar 2026 21:44:19 -0500 Subject: [PATCH] Wire store checkout flow across client and server - Add checkout request/response bridge and workspace re-hydration in store UI - Implement server-side checkout stores for charging bank/cash and granting locker/VA items - Normalize catalog/cart payload categories and fix locker VA sync event naming --- .../addons/locker/XEH_postInitClient.sqf | 4 +- .../locker/functions/fnc_initVAClass.sqf | 16 +- .../addons/org/ui/_site/logic/portalStore.js | 3 - arma/client/addons/org/ui/_site/runtime.js | 163 ++++++- .../addons/org/ui/_site/useRegistryStore.js | 4 +- .../addons/store/XEH_postInitClient.sqf | 6 + .../store/functions/fnc_initStoreUIBridge.sqf | 65 ++- arma/client/addons/store/ui/_site/bridge.js | 6 + .../addons/store/ui/_site/logic/events.js | 66 ++- .../addons/store/ui/_site/logic/store.js | 59 ++- .../bank/functions/fnc_initBankStore.sqf | 56 +++ arma/server/addons/locker/XEH_preInit.sqf | 8 +- .../locker/functions/fnc_initLockerStore.sqf | 62 +++ .../locker/functions/fnc_initVAStore.sqf | 47 ++ .../addons/main/functions/fnc_initStores.sqf | 3 + arma/server/addons/org/XEH_PREP.hpp | 2 + .../addons/org/functions/fnc_initOrgStore.sqf | 433 ++++-------------- .../org/functions/fnc_memberService.sqf | 243 ++++++++++ .../org/functions/fnc_treasuryService.sqf | 164 +++++++ arma/server/addons/store/XEH_PREP.hpp | 3 +- arma/server/addons/store/XEH_preInit.sqf | 14 + .../store/functions/fnc_initStoreStore.sqf | 168 +++++++ lib/repositories/src/org.rs | 10 + lib/services/src/org.rs | 58 ++- 24 files changed, 1218 insertions(+), 445 deletions(-) create mode 100644 arma/server/addons/org/functions/fnc_memberService.sqf create mode 100644 arma/server/addons/org/functions/fnc_treasuryService.sqf create mode 100644 arma/server/addons/store/functions/fnc_initStoreStore.sqf diff --git a/arma/client/addons/locker/XEH_postInitClient.sqf b/arma/client/addons/locker/XEH_postInitClient.sqf index f6152bd..d4baf95 100644 --- a/arma/client/addons/locker/XEH_postInitClient.sqf +++ b/arma/client/addons/locker/XEH_postInitClient.sqf @@ -10,7 +10,7 @@ if (isNil QGVAR(VAClass)) then { call FUNC(initVAClass); }; [QGVAR(responseInitLocker), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(LockerClass) call ["sync", [_data, true]]; + GVAR(LockerClass) call ["sync", [_data]]; }] call CFUNC(addEventHandler); [QGVAR(responseSyncLocker), { @@ -26,7 +26,7 @@ if (isNil QGVAR(VAClass)) then { call FUNC(initVAClass); }; [QGVAR(responseInitVA), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(VAClass) call ["sync", [_data, true]]; + GVAR(VAClass) call ["sync", [_data]]; }] call CFUNC(addEventHandler); [QGVAR(responseSyncVA), { diff --git a/arma/client/addons/locker/functions/fnc_initVAClass.sqf b/arma/client/addons/locker/functions/fnc_initVAClass.sqf index a40cd20..5711d63 100644 --- a/arma/client/addons/locker/functions/fnc_initVAClass.sqf +++ b/arma/client/addons/locker/functions/fnc_initVAClass.sqf @@ -45,7 +45,7 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [ _self set ["lastSave", time]; }], ["sync", compileFinal { - params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; + params [["_data", createHashMap, [createHashMap]]]; private _vArsenal = _self get "vArsenal"; private _isLoaded = _self get "isLoaded"; @@ -53,14 +53,12 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [ { _vArsenal set [_x, _y]; - if (_jip) then { - switch (_x) do { - case "items": { _self call ["applyItems", []]; }; - case "weapons": { _self call ["applyWeapons", []]; }; - case "magazines": { _self call ["applyMagazines", []]; }; - case "backpacks": { _self call ["applyBackpacks", []]; }; - default {}; - }; + switch (_x) do { + case "items": { _self call ["applyItems", []]; }; + case "weapons": { _self call ["applyWeapons", []]; }; + case "magazines": { _self call ["applyMagazines", []]; }; + case "backpacks": { _self call ["applyBackpacks", []]; }; + default {}; }; } forEach _data; diff --git a/arma/client/addons/org/ui/_site/logic/portalStore.js b/arma/client/addons/org/ui/_site/logic/portalStore.js index 2d52184..c9e1814 100644 --- a/arma/client/addons/org/ui/_site/logic/portalStore.js +++ b/arma/client/addons/org/ui/_site/logic/portalStore.js @@ -29,9 +29,6 @@ this.setFunds(payload.portalData.funds || 0); this.setMembers([...(payload.portalData.members || [])]); this.setCreditLines([...(payload.portalData.creditLines || [])]); - this.setTreasuryNotice({ type: "", text: "" }); - this.setModal(null); - this.setOrgDisbanded(false); } } diff --git a/arma/client/addons/org/ui/_site/runtime.js b/arma/client/addons/org/ui/_site/runtime.js index 742ebb7..73b768f 100644 --- a/arma/client/addons/org/ui/_site/runtime.js +++ b/arma/client/addons/org/ui/_site/runtime.js @@ -20,6 +20,26 @@ "mask", ]); + function appendChild(el, child) { + if (child === null || child === undefined || child === false) { + return; + } + + if (Array.isArray(child)) { + child.forEach((entry) => appendChild(el, entry)); + return; + } + + if (typeof child === "string" || typeof child === "number") { + el.appendChild(document.createTextNode(String(child))); + return; + } + + if (child instanceof Node) { + el.appendChild(child); + } + } + function h(tag, props = {}, ...children) { const isSvg = SVG_TAGS.has(tag); const el = isSvg @@ -30,44 +50,132 @@ Object.entries(props).forEach(([key, value]) => { if (key.startsWith("on") && typeof value === "function") { el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === "className") { + return; + } + + if (key === "className") { if (isSvg) { el.setAttribute("class", value); } else { el.className = value; } - } else if (key === "style" && typeof value === "object") { + return; + } + + if (key === "style" && typeof value === "object") { Object.assign(el.style, value); - } else if (typeof value === "boolean") { + return; + } + + if (key === "value" && "value" in el) { + el.value = value; + return; + } + + if (key === "checked" && "checked" in el) { + el.checked = Boolean(value); + return; + } + + if (typeof value === "boolean") { if (value) { el.setAttribute(key, ""); } else { el.removeAttribute(key); } - } else if (value === null || value === undefined) { - el.removeAttribute(key); - } else { - el.setAttribute(key, value); + return; } + + if (value === null || value === undefined) { + el.removeAttribute(key); + return; + } + + el.setAttribute(key, value); }); } - children.forEach((child) => { - if (typeof child === "string" || typeof child === "number") { - el.appendChild(document.createTextNode(child)); - } else if (child instanceof Node) { - el.appendChild(child); - } else if (Array.isArray(child)) { - child.forEach((c) => el.appendChild(c)); - } - }); - + children.forEach((child) => appendChild(el, child)); return el; } let rootContainer = null; let rootComponent = null; const injectedStyles = new Set(); + let rerenderQueued = false; + + function captureScrollState(container) { + if (!container) { + return []; + } + + return Array.from( + container.querySelectorAll("[data-preserve-scroll-id]"), + ).map((node) => ({ + id: node.getAttribute("data-preserve-scroll-id"), + scrollTop: node.scrollTop, + scrollLeft: node.scrollLeft, + })); + } + + function captureViewportScroll(container) { + if (!container) { + return { scrollTop: 0, scrollLeft: 0 }; + } + + const viewport = container.querySelector("main"); + if (!viewport) { + return { + scrollTop: window.scrollY || window.pageYOffset || 0, + scrollLeft: window.scrollX || window.pageXOffset || 0, + }; + } + + return { + scrollTop: viewport.scrollTop, + scrollLeft: viewport.scrollLeft, + }; + } + + function restoreScrollState(container, entries) { + if (!container || !Array.isArray(entries) || entries.length === 0) { + return; + } + + entries.forEach((entry) => { + if (!entry || !entry.id) { + return; + } + + const target = container.querySelector( + `[data-preserve-scroll-id="${entry.id}"]`, + ); + if (!target) { + return; + } + + target.scrollTop = Number(entry.scrollTop || 0); + target.scrollLeft = Number(entry.scrollLeft || 0); + }); + } + + function restoreViewportScroll(container, viewportScroll) { + if (!container || !viewportScroll) { + return; + } + + const viewport = container.querySelector("main"); + if (!viewport) { + window.scrollTo( + Number(viewportScroll.scrollLeft || 0), + Number(viewportScroll.scrollTop || 0), + ); + return; + } + + viewport.scrollTop = Number(viewportScroll.scrollTop || 0); + viewport.scrollLeft = Number(viewportScroll.scrollLeft || 0); + } function render(component, container) { rootContainer = container; @@ -75,13 +183,31 @@ rerender(); } - function rerender() { + function flushRerender() { if (!rootContainer || !rootComponent) { return; } + const viewportScroll = captureViewportScroll(rootContainer); + const scrollState = captureScrollState(rootContainer); rootContainer.innerHTML = ""; rootContainer.appendChild(rootComponent()); + requestAnimationFrame(() => { + restoreScrollState(rootContainer, scrollState); + restoreViewportScroll(rootContainer, viewportScroll); + }); + } + + function rerender() { + if (rerenderQueued) { + return; + } + + rerenderQueued = true; + requestAnimationFrame(() => { + rerenderQueued = false; + flushRerender(); + }); } function ensureScopedStyle(id, cssText) { @@ -111,6 +237,7 @@ const runtime = { h, render, + rerender, createSignal, ensureScopedStyle, }; diff --git a/arma/client/addons/org/ui/_site/useRegistryStore.js b/arma/client/addons/org/ui/_site/useRegistryStore.js index c4c0106..ae2a830 100644 --- a/arma/client/addons/org/ui/_site/useRegistryStore.js +++ b/arma/client/addons/org/ui/_site/useRegistryStore.js @@ -16,7 +16,9 @@ OrgPortal.data.applyLoginPayload(payload); OrgPortal.store.hydrateFromPayload(payload); - RegistryApp.store.setView("portal"); + if (RegistryApp.store.getView() !== "portal") { + RegistryApp.store.setView("portal"); + } return true; }, }); diff --git a/arma/client/addons/store/XEH_postInitClient.sqf b/arma/client/addons/store/XEH_postInitClient.sqf index ec0f679..b2cd6ca 100644 --- a/arma/client/addons/store/XEH_postInitClient.sqf +++ b/arma/client/addons/store/XEH_postInitClient.sqf @@ -2,3 +2,9 @@ if (isNil QGVAR(StoreClass)) then { call FUNC(initStoreClass); }; if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initStoreUIBridge); }; + +[QGVAR(responseCheckout), { + params [["_payload", createHashMap, [createHashMap]]]; + + GVAR(StoreUIBridge) call ["handleCheckoutResponse", [_payload]]; +}] call CFUNC(addEventHandler); diff --git a/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf b/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf index d5d99d4..e626919 100644 --- a/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf +++ b/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf @@ -246,6 +246,28 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [ _sortedItems sort true; _sortedItems apply { _x select 1 } }], + ["isVehicleCategory", compileFinal { + params [["_category", "", [""]]]; + + (toLowerANSI _category) in ["cars", "armor", "helis", "planes", "naval", "other"] + }], + ["buildPayloadCategory", compileFinal { + params [["_category", "", [""]]]; + + switch (toLowerANSI _category) do { + case "ammo": { "magazine" }; + case "primary"; + case "secondary"; + case "handgun": { "weapon" }; + case "cars"; + case "armor"; + case "helis"; + case "planes"; + case "naval"; + case "other": { toLowerANSI _category }; + default { "item" }; + } + }], ["buildCategoryItems", compileFinal { params [["_category", "", [""]]]; @@ -256,6 +278,14 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [ if (_categoryKey in (keys _catalogCache)) exitWith { _catalogCache get _categoryKey }; private _items = _self call ["scanCategoryItems", [_categoryKey]]; + private _payloadCategory = _self call ["buildPayloadCategory", [_categoryKey]]; + private _entryKind = ["item", "vehicle"] select (_self call ["isVehicleCategory", [_categoryKey]]); + + { + _x set ["category", _payloadCategory]; + _x set ["entryKind", _entryKind]; + } forEach _items; + _catalogCache set [_categoryKey, _items]; _self set ["catalogCache", _catalogCache]; @@ -298,15 +328,40 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ["items", _items] ]]]; }], + ["refreshWorkspace", compileFinal { + private _payload = GVAR(StoreClass) call ["buildUIPayload", []]; + _self call ["sendBridgeEvent", ["store::workspace::hydrate", _payload]]; + }], ["handleCheckoutRequest", compileFinal { params [["_data", createHashMap, [createHashMap]]]; - private _items = _data getOrDefault ["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]; + private _uid = getPlayerUID player; + private _checkoutJson = _data getOrDefault ["checkoutJson", ""]; - diag_log format ["[FORGE:Client:Store] Checkout request received: %1", _data]; - _self call ["sendBridgeEvent", ["store::checkout::failure", createHashMapFromArray [["message", _message]]]]; + if (_uid isEqualTo "" || { _checkoutJson isEqualTo "" }) exitWith { + _self call ["sendBridgeEvent", ["store::checkout::failure", createHashMapFromArray [ + ["message", "Add at least one supported item before checkout."] + ]]]; + }; + + diag_log format ["[FORGE:Client:Store] Checkout request forwarded to server: %1", _checkoutJson]; + [SRPC(store,requestCheckout), [_uid, _checkoutJson]] call CFUNC(serverEvent); + }], + ["handleCheckoutResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; + + private _success = _payload getOrDefault ["success", false]; + private _bridgeEvent = ["store::checkout::failure", "store::checkout::success"] select _success; + _self call ["sendBridgeEvent", [_bridgeEvent, _payload]]; + + if (_success) then { + [] spawn { + sleep 0.05; + if !(isNil QGVAR(StoreUIBridge)) then { + GVAR(StoreUIBridge) call ["refreshWorkspace", []]; + }; + }; + }; }] ]; diff --git a/arma/client/addons/store/ui/_site/bridge.js b/arma/client/addons/store/ui/_site/bridge.js index 3f61db0..9b669ab 100644 --- a/arma/client/addons/store/ui/_site/bridge.js +++ b/arma/client/addons/store/ui/_site/bridge.js @@ -51,6 +51,12 @@ return; } + if (event === "store::workspace::hydrate") { + StorefrontApp.data.applyHydratePayload(payloadData); + store.hydrateWorkspace(payloadData); + return; + } + if (event === "store::checkout::success") { store.setIsCheckingOut(false); store.setCartItems([]); diff --git a/arma/client/addons/store/ui/_site/logic/events.js b/arma/client/addons/store/ui/_site/logic/events.js index a9203d4..14cabc9 100644 --- a/arma/client/addons/store/ui/_site/logic/events.js +++ b/arma/client/addons/store/ui/_site/logic/events.js @@ -19,6 +19,54 @@ }, 3200); } + function normalizeCheckoutItem(item) { + return { + classname: String(item?.code || "").trim(), + category: String(item?.category || "") + .trim() + .toLowerCase(), + entryKind: String(item?.entryKind || "item") + .trim() + .toLowerCase(), + quantity: Math.max(1, Number(item?.quantity || 1)), + }; + } + + function buildCheckoutPayload(cartItems, paymentMethod, totalPrice) { + const payload = { + items: [], + vehicles: [], + totalPrice, + paymentMethod, + }; + + cartItems.forEach((item) => { + const normalizedItem = normalizeCheckoutItem(item); + + if (normalizedItem.entryKind === "vehicle") { + for ( + let index = 0; + index < normalizedItem.quantity; + index += 1 + ) { + payload.vehicles.push({ + classname: normalizedItem.classname, + category: normalizedItem.category, + }); + } + return; + } + + payload.items.push({ + classname: normalizedItem.classname, + category: normalizedItem.category, + quantity: normalizedItem.quantity, + }); + }); + + return payload; + } + function applySearchQuery(value) { store.setSearchQuery(String(value || "").trim()); store.resetCatalogPage(); @@ -149,6 +197,8 @@ code: item.code, name: item.name, price: item.price, + category: item.category, + entryKind: item.entryKind, quantity: 1, }, ]; @@ -159,6 +209,8 @@ {}, nextItems[existingIndex], { + category: item.category, + entryKind: item.entryKind, quantity: nextItems[existingIndex].quantity + 1, }, ); @@ -266,14 +318,14 @@ store.setIsCheckingOut(true); + const checkoutPayload = buildCheckoutPayload( + cartItems, + selectedPaymentSource.id, + summary.total, + ); + const sent = bridge.requestCheckout({ - actorUid: session.actorUid, - actorName: session.actorName, - paymentMethod: selectedPaymentSource.id, - paymentLabel: selectedPaymentSource.label, - items: cartItems, - subtotal: summary.subtotal, - total: summary.total, + checkoutJson: JSON.stringify(checkoutPayload), }); if (!sent) { diff --git a/arma/client/addons/store/ui/_site/logic/store.js b/arma/client/addons/store/ui/_site/logic/store.js index 86132a0..f6c0c49 100644 --- a/arma/client/addons/store/ui/_site/logic/store.js +++ b/arma/client/addons/store/ui/_site/logic/store.js @@ -4,6 +4,32 @@ SharedLogic.createStorefrontStore = function createStorefrontStore({ createSignal, }) { + function normalizeCatalogItem(item) { + return { + className: String(item?.className || item?.code || ""), + code: String(item?.code || item?.className || ""), + name: String(item?.name || item?.displayName || ""), + description: String(item?.description || ""), + price: String(item?.price || ""), + image: String(item?.image || ""), + type: String(item?.type || ""), + category: String(item?.category || ""), + entryKind: String(item?.entryKind || "item"), + quantity: Math.max(0, Number(item?.quantity || 0)), + }; + } + + function normalizeCartItem(item) { + return { + code: String(item?.code || ""), + name: String(item?.name || ""), + price: String(item?.price || "$0"), + category: String(item?.category || ""), + entryKind: String(item?.entryKind || "item"), + quantity: Math.max(1, Number(item?.quantity || 1)), + }; + } + class StorefrontStore { constructor() { [this.getView, this.setView] = createSignal("categories"); @@ -147,18 +173,7 @@ this.setCatalogItemsByKey((currentItemsByKey) => Object.assign({}, currentItemsByKey, { - [categoryKey]: items.map((item) => ({ - className: String( - item.className || item.code || "", - ), - code: String(item.code || item.className || ""), - name: String(item.name || item.displayName || ""), - description: String(item.description || ""), - price: String(item.price || ""), - image: String(item.image || ""), - type: String(item.type || ""), - quantity: Math.max(0, Number(item.quantity || 0)), - })), + [categoryKey]: items.map(normalizeCatalogItem), }), ); @@ -239,12 +254,7 @@ : []; this.setCartItems( - cartItems.map((item) => ({ - code: String(item.code || ""), - name: String(item.name || ""), - price: String(item.price || "$0"), - quantity: Math.max(1, Number(item.quantity || 1)), - })), + cartItems.map(normalizeCartItem), ); this.setCartOpen(false); this.setIsCheckingOut(false); @@ -256,6 +266,19 @@ payload?.workspace || payload?.storeConfig || {}, ); } + + hydrateWorkspace(payload) { + const cartItems = Array.isArray(payload?.cartItems) + ? payload.cartItems + : []; + + this.setCartItems(cartItems.map(normalizeCartItem)); + this.setCartOpen(false); + this.setIsCheckingOut(false); + this.ensureSelectedPaymentSource( + payload?.workspace || payload?.storeConfig || {}, + ); + } } return new StorefrontStore(); diff --git a/arma/server/addons/bank/functions/fnc_initBankStore.sqf b/arma/server/addons/bank/functions/fnc_initBankStore.sqf index ef619e7..f130777 100644 --- a/arma/server/addons/bank/functions/fnc_initBankStore.sqf +++ b/arma/server/addons/bank/functions/fnc_initBankStore.sqf @@ -195,6 +195,62 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Paid $%1", _amount]], _player] call CFUNC(targetEvent); }], + ["buildChargeResult", compileFinal { + params [["_message", "Unable to process bank payment.", [""]]]; + + createHashMapFromArray [ + ["success", false], + ["message", _message], + ["patch", createHashMap] + ] + }], + ["chargeCheckout", compileFinal { + params [ + ["_uid", "", [""]], + ["_source", "cash", [""]], + ["_amount", 0, [0]], + ["_commit", false, [false]] + ]; + + private _result = _self call ["buildChargeResult", []]; + private _field = switch (toLowerANSI _source) do { + case "cash": { "cash" }; + case "bank": { "bank" }; + default { "" }; + }; + + if (_field isEqualTo "") exitWith { + _result set ["message", "Selected bank payment source is unsupported."]; + _result + }; + + private _account = GVAR(Registry) getOrDefault [_uid, createHashMap]; + if (_account isEqualTo createHashMap) exitWith { + _result set ["message", "Bank account data is unavailable for checkout."]; + _result + }; + + private _balance = _account getOrDefault [_field, 0]; + if (_balance < _amount) exitWith { + private _message = [ + "Bank balance cannot cover this checkout.", + "Cash on hand cannot cover this checkout." + ] select (_field isEqualTo "cash"); + + _result set ["message", _message]; + _result + }; + + private _patch = createHashMapFromArray [[_field, (_balance - _amount)]]; + if (_commit) then { + _patch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]]; + }; + + _result set ["success", true]; + _result set ["message", ""]; + _result set ["patch", _patch]; + _result + }], ["transfer", compileFinal { params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]]; diff --git a/arma/server/addons/locker/XEH_preInit.sqf b/arma/server/addons/locker/XEH_preInit.sqf index 9ee55ce..a1b5d92 100644 --- a/arma/server/addons/locker/XEH_preInit.sqf +++ b/arma/server/addons/locker/XEH_preInit.sqf @@ -90,7 +90,7 @@ PREP_RECOMPILE_END; private _finalData = GVAR(VAStore) call ["get", [GVAR(VARegistry), "owned:locker:fetch", _uid, _field]]; private _player = [_uid] call EFUNC(common,getPlayer); - [CRPC(locker,responseSyncVArsenal), [_finalData], _player] call CFUNC(targetEvent); + [CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); [QGVAR(requestSetVA), { @@ -101,7 +101,7 @@ PREP_RECOMPILE_END; private _hashMap = GVAR(VAStore) call ["set", [GVAR(VARegistry), "owned:locker:update", _uid, _field, _value, _sync]]; private _player = [_uid] call EFUNC(common,getPlayer); - [CRPC(locker,responseSyncVArsenal), [_hashMap], _player] call CFUNC(targetEvent); + [CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); [QGVAR(requestMSetVA), { @@ -113,7 +113,7 @@ PREP_RECOMPILE_END; private _hashMap = GVAR(VAStore) call ["mset", [GVAR(VARegistry), "owned:locker:update", _uid, _fieldValuePairs, _sync]]; private _player = [_uid] call EFUNC(common,getPlayer); - [CRPC(locker,responseSyncVArsenal), [_hashMap], _player] call CFUNC(targetEvent); + [CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); [QGVAR(requestSaveVA), { @@ -124,7 +124,7 @@ PREP_RECOMPILE_END; private _finalData = GVAR(VAStore) call ["save", [GVAR(VARegistry), "owned:locker:update", _uid]]; private _player = [_uid] call EFUNC(common,getPlayer); - [CRPC(locker,responseSyncVArsenal), [_finalData], _player] call CFUNC(targetEvent); + [CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); [QGVAR(requestRemoveVA), { diff --git a/arma/server/addons/locker/functions/fnc_initLockerStore.sqf b/arma/server/addons/locker/functions/fnc_initLockerStore.sqf index efb085b..352c940 100644 --- a/arma/server/addons/locker/functions/fnc_initLockerStore.sqf +++ b/arma/server/addons/locker/functions/fnc_initLockerStore.sqf @@ -70,6 +70,68 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [ [CRPC(locker,responseInitLocker), [_finalLocker], _player] call CFUNC(targetEvent); _finalLocker + }], + ["grantItems", compileFinal { + params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Locker grant failed."], + ["patch", createHashMap], + ["granted", []], + ["locker", createHashMap] + ]; + + private _locker = +(GVAR(Registry) getOrDefault [_uid, createHashMap]); + 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": { "item" }; + case "weapon": { "weapon" }; + case "magazine": { "magazine" }; + case "backpack": { "backpack" }; + default { "" }; + }; + + if (_className isEqualTo "" || { _lockerCategory isEqualTo "" } || { _quantity <= 0 }) exitWith { + _result set ["message", "Checkout item was missing a valid classname, category, or quantity."]; + _result set ["success", false]; + }; + + 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 { GVAR(Registry) set [_uid, _locker]; }; + + _result set ["success", true]; + _result set ["message", ""]; + _result set ["patch", _patch]; + _result set ["granted", _granted]; + _result set ["locker", _locker]; + _result }] ]; diff --git a/arma/server/addons/locker/functions/fnc_initVAStore.sqf b/arma/server/addons/locker/functions/fnc_initVAStore.sqf index 92aecd5..809fa95 100644 --- a/arma/server/addons/locker/functions/fnc_initVAStore.sqf +++ b/arma/server/addons/locker/functions/fnc_initVAStore.sqf @@ -89,6 +89,53 @@ GVAR(VABaseStore) = compileFinal createHashMapFromArray [ [CRPC(locker,responseInitVA), [_finalVArsenal], _player] call CFUNC(targetEvent); _finalVArsenal + }], + ["unlockItems", compileFinal { + params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "VA unlock failed."], + ["patch", createHashMap], + ["arsenal", createHashMap] + ]; + + private _defaultArsenal = GVAR(VArsenalModel) call ["defaults", []]; + private _arsenal = +(GVAR(VARegistry) getOrDefault [_uid, _defaultArsenal]); + 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 { GVAR(VARegistry) set [_uid, _arsenal]; }; + + _result set ["success", true]; + _result set ["message", ""]; + _result set ["patch", _patch]; + _result set ["arsenal", _arsenal]; + _result }] ]; diff --git a/arma/server/addons/main/functions/fnc_initStores.sqf b/arma/server/addons/main/functions/fnc_initStores.sqf index 4151f9a..9093c32 100644 --- a/arma/server/addons/main/functions/fnc_initStores.sqf +++ b/arma/server/addons/main/functions/fnc_initStores.sqf @@ -39,3 +39,6 @@ if (isNil QEGVAR(locker,VAStore)) then { call EFUNC(locker,initVAStore); }; // Org if (isNil QEGVAR(org,OrgStore)) then { call EFUNC(org,initOrgStore); }; + +// Store +if (isNil QEGVAR(store,StoreStore)) then { call EFUNC(store,initStoreStore); }; diff --git a/arma/server/addons/org/XEH_PREP.hpp b/arma/server/addons/org/XEH_PREP.hpp index c7b9330..dc78ebd 100644 --- a/arma/server/addons/org/XEH_PREP.hpp +++ b/arma/server/addons/org/XEH_PREP.hpp @@ -1 +1,3 @@ PREP(initOrgStore); +PREP(memberService); +PREP(treasuryService); diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf index 9c9ff53..5ef0327 100644 --- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf +++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf @@ -4,7 +4,7 @@ * File: fnc_initOrgStore.sqf * Author: IDSolutions * Date: 2026-02-13 - * Last Update: 2026-02-13 + * Last Update: 2026-03-13 * Public: Yes * * Description: @@ -21,6 +21,9 @@ * 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"], @@ -43,7 +46,6 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[ params [["_org", createHashMap, [createHashMap]]]; private _defaults = _self call ["defaults", []]; - { if !(_x in _org) then { _org set [_x, _y]; }; } forEach _defaults; @@ -104,115 +106,31 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _defaultOrg }; - private _finalOrg = createHashMap; - + private _defaultOrg = createHashMap; if (_result == "true") then { - _finalOrg = _self call ["fetch", ["org:get", "default"]]; + _defaultOrg = _self call ["fetch", ["org:get", "default"]]; } else { - _finalOrg set ["id", "default"]; - _finalOrg set ["owner", "server"]; - _finalOrg set ["name", "Forge Dynamics"]; - _finalOrg set ["funds", 200000]; - _finalOrg set ["reputation", 0]; - _finalOrg set ["credit_lines", createHashMap]; + _defaultOrg set ["id", "default"]; + _defaultOrg set ["owner", "server"]; + _defaultOrg set ["name", "Forge Dynamics"]; + _defaultOrg set ["funds", 200000]; + _defaultOrg set ["reputation", 0]; + _defaultOrg set ["credit_lines", createHashMap]; - private _json = _self call ["toJSON", [_finalOrg]]; - ["org:create", ["default", _json]] call EFUNC(extension,extCall); + private _defaultJson = _self call ["toJSON", [_defaultOrg]]; + ["org:create", ["default", _defaultJson]] call EFUNC(extension,extCall); }; - GVAR(Registry) set ["default", _finalOrg]; + GVAR(Registry) set ["default", _defaultOrg]; }], ["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 _finalMembers = +_members; - _finalMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]]; - _org set ["members", _finalMembers]; - - _org - }], - ["loadById", compileFinal { - params [["_orgID", "", [""]]]; - - if (_orgID isEqualTo "") exitWith { createHashMap }; - - private _cached = GVAR(Registry) getOrDefault [_orgID, createHashMap]; - if (_cached isNotEqualTo createHashMap) exitWith { _cached }; - - ["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"]; - if (!_existsSuccess || { _existsResult isNotEqualTo "true" }) exitWith { createHashMap }; - - 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 = []; }; - - private _memberMap = createHashMap; - { - private _memberUid = _x getOrDefault ["uid", ""]; - if (_memberUid isNotEqualTo "") then { _memberMap set [_memberUid, _x]; }; - } forEach _memberRows; - - _org set ["members", _memberMap]; - GVAR(Registry) set [_orgID, _org, true]; - _org + GVAR(OrgMembershipService) call ["verifyMember", _this] }], ["addMember", compileFinal { - params [ - ["_orgID", "", [""]], - ["_uid", "", [""]], - ["_player", objNull, [objNull]], - ["_actor", createHashMap, [createHashMap]] - ]; - - if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; - - private _org = _self call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { _org }; - - _org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]]; - GVAR(Registry) set [_orgID, _org, true]; - _org + GVAR(OrgMembershipService) call ["addMember", _this] }], ["removeMember", compileFinal { - params [ - ["_orgID", "", [""]], - ["_uid", "", [""]] - ]; - - if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; - - private _org = _self 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 _members = +(_org getOrDefault ["members", createHashMap]); - _members deleteAt _uid; - _org set ["members", _members]; - GVAR(Registry) set [_orgID, _org, true]; - - _org + GVAR(OrgMembershipService) call ["removeMember", _this] }], ["delete", compileFinal { params [["_orgID", "", [""]]]; @@ -238,256 +156,55 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }], ["restoreDefaultMembership", compileFinal { - params [ - ["_uid", "", [""]], - ["_player", objNull, [objNull]], - ["_actor", createHashMap, [createHashMap]] - ]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", ""], - ["actorPatch", createHashMap] - ]; - - 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", true]]; - private _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, EGVAR(actor,Registry) getOrDefault [_uid, _resolvedActor]]]; - if (_defaultOrg isEqualTo createHashMap) exitWith { - _result set ["message", "Failed to restore default organization membership."]; - _result - }; - - GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", "default"]]]; - _result set ["success", true]; - _result set ["actorPatch", _actorPatch]; - _result + GVAR(OrgMembershipService) call ["restoreDefaultMembership", _this] }], ["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 = _self 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 + GVAR(OrgMembershipService) call ["leave", _this] }], ["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 = _self 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 = _self 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 _defaultResult = _self call ["restoreDefaultMembership", [_memberUid, _memberPlayer, EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]]]; - 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) call ["disband", _this] }], ["assignCreditLine", compileFinal { - params [ - ["_requesterUid", "", [""]], - ["_memberUid", "", [""]], - ["_memberName", "", [""]], - ["_amount", 0, [0]] - ]; + GVAR(OrgTreasuryService) call ["assignCreditLine", _this] + }], + ["buildChargeResult", compileFinal { + GVAR(OrgTreasuryService) call ["buildChargeResult", _this] + }], + ["chargeCheckout", compileFinal { + GVAR(OrgTreasuryService) call ["chargeCheckout", _this] + }], + ["loadById", compileFinal { + params [["_orgID", "", [""]]]; - private _result = createHashMapFromArray [ - ["success", false], - ["message", ""], - ["patch", createHashMap], - ["memberUids", []] - ]; + if (_orgID isEqualTo "") exitWith { createHashMap }; - if ( - _requesterUid isEqualTo "" - || { _memberUid isEqualTo "" } - || { _amount <= 0 } - ) exitWith { - _result set ["message", "A valid requester, member, and credit amount are required."]; - _result + private _cachedOrg = GVAR(Registry) getOrDefault [_orgID, createHashMap]; + if (_cachedOrg isNotEqualTo createHashMap) exitWith { _cachedOrg }; + + ["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"]; + if (!_existsSuccess || { _existsResult isNotEqualTo "true" }) exitWith { createHashMap }; + + 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 = []; }; - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - private _orgID = _requesterActor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { - _orgID = "default"; - }; + private _memberMap = createHashMap; + { + private _memberUid = _x getOrDefault ["uid", ""]; + if (_memberUid isNotEqualTo "") then { + _memberMap set [_memberUid, _x]; + }; + } forEach _memberRows; - 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 + _org set ["members", _memberMap]; + GVAR(Registry) set [_orgID, _org, true]; + _org }], ["register", compileFinal { params [["_uid", "", [""]], ["_orgName", "", [""]]]; @@ -507,7 +224,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ 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 @@ -549,7 +265,9 @@ 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]]; @@ -576,18 +294,20 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ private _player = [_uid] call EFUNC(common,getPlayer); private _actor = EGVAR(actor,Registry) get _uid; private _orgID = _actor get "organization"; - if (_orgID isEqualTo "") then { _orgID = "default" }; + if (_orgID isEqualTo "") then { + _orgID = "default"; + }; - private _cached = GVAR(Registry) getOrDefault [_orgID, nil]; - if !(isNil { _cached }) exitWith { - private _cachedOwner = _cached getOrDefault ["owner", ""]; + private _cachedOrg = GVAR(Registry) getOrDefault [_orgID, nil]; + if !(isNil { _cachedOrg }) exitWith { + private _cachedOwner = _cachedOrg getOrDefault ["owner", ""]; if (_orgID isEqualTo "default" || { _cachedOwner isEqualTo _uid }) then { - _cached = _self call ["verifyMember", [_cached, _orgID, _uid, _player, _actor]]; + _cachedOrg = _self call ["verifyMember", [_cachedOrg, _orgID, _uid, _player, _actor]]; }; - GVAR(Registry) set [_orgID, _cached, true]; - [CRPC(org,responseInitOrg), [_cached], _player] call CFUNC(targetEvent); + GVAR(Registry) set [_orgID, _cachedOrg, true]; + [CRPC(org,responseInitOrg), [_cachedOrg], _player] call CFUNC(targetEvent); - _cached + _cachedOrg }; ["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; @@ -595,18 +315,19 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["ERROR", format ["Failed to check for org %1! Using fallback org.", _orgID]] call EFUNC(common,log); private _fallbackOrg = GVAR(Registry) getOrDefault ["default", createHashMap]; - private _entry = createHashMapFromArray [["orgID", _orgID]]; - GVAR(IndexRegistry) set [_uid, _entry]; + GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]]; + + if (_orgID isEqualTo "default") then { + _fallbackOrg = _self call ["verifyMember", [_fallbackOrg, _orgID, _uid, _player, _actor]]; + }; - if (_orgID isEqualTo "default") then { _fallbackOrg = _self call ["verifyMember", [_fallbackOrg, _orgID, _uid, _player, _actor]]; }; GVAR(Registry) set [_orgID, _fallbackOrg, true]; [CRPC(org,responseInitOrg), [_fallbackOrg], _player] call CFUNC(targetEvent); - _fallbackOrg; + _fallbackOrg }; private _finalOrg = createHashMap; - if (_result == "true") then { _finalOrg = _self call ["loadById", [_orgID]]; ["INFO", format ["Found org for %1", _orgID]] call EFUNC(common,log); @@ -616,13 +337,13 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _orgID = "default"; }; - private _entry = createHashMapFromArray [["orgID", _orgID]]; - GVAR(IndexRegistry) set [_uid, _entry]; + GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]]; private _finalOwner = _finalOrg getOrDefault ["owner", ""]; if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then { _finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]]; }; + GVAR(Registry) set [_orgID, _finalOrg, true]; [CRPC(org,responseInitOrg), [_finalOrg], _player] call CFUNC(targetEvent); diff --git a/arma/server/addons/org/functions/fnc_memberService.sqf b/arma/server/addons/org/functions/fnc_memberService.sqf new file mode 100644 index 0000000..7e47105 --- /dev/null +++ b/arma/server/addons/org/functions/fnc_memberService.sqf @@ -0,0 +1,243 @@ +#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 + }], + ["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]]; + GVAR(Registry) set [_orgID, _org, true]; + + _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]; + GVAR(Registry) set [_orgID, _org, true]; + + _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", true]]; + private _defaultActor = EGVAR(actor,Registry) getOrDefault [_uid, _resolvedActor]; + private _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, _defaultActor]]; + if (_defaultOrg isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to restore default organization membership."]; + _result + }; + + GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", "default"]]]; + _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)]; diff --git a/arma/server/addons/org/functions/fnc_treasuryService.sqf b/arma/server/addons/org/functions/fnc_treasuryService.sqf new file mode 100644 index 0000000..33cffe8 --- /dev/null +++ b/arma/server/addons/org/functions/fnc_treasuryService.sqf @@ -0,0 +1,164 @@ +#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 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", [GVAR(Registry), "org:update", _orgID, "credit_lines", _creditLines, true]]; + private _memberUids = _self call ["resolveOrgMemberUids", [_org, _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 + }], + ["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(Registry) getOrDefault [_orgID, createHashMap]; + 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", [GVAR(Registry), "org:update", _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", [GVAR(Registry), "org:update", _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)]; diff --git a/arma/server/addons/store/XEH_PREP.hpp b/arma/server/addons/store/XEH_PREP.hpp index 7845920..7c098e7 100644 --- a/arma/server/addons/store/XEH_PREP.hpp +++ b/arma/server/addons/store/XEH_PREP.hpp @@ -1,2 +1 @@ -// PREP(initStore); -// PREP(initStoreStore); +PREP(initStoreStore); diff --git a/arma/server/addons/store/XEH_preInit.sqf b/arma/server/addons/store/XEH_preInit.sqf index 9dd4ebf..dc3d5a0 100644 --- a/arma/server/addons/store/XEH_preInit.sqf +++ b/arma/server/addons/store/XEH_preInit.sqf @@ -5,3 +5,17 @@ PREP_RECOMPILE_START; PREP_RECOMPILE_END; // private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; + +[QGVAR(requestCheckout), { + params [["_uid", "", [""]], ["_payloadJson", "", [""]]]; + + if (_uid isEqualTo "" || { _payloadJson isEqualTo "" }) exitWith { + diag_log "[FORGE:Server:Store] Invalid checkout request payload." + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _result = GVAR(StoreStore) call ["checkout", [_uid, _player, _payloadJson]]; + [CRPC(store,responseCheckout), [_result], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/store/functions/fnc_initStoreStore.sqf b/arma/server/addons/store/functions/fnc_initStoreStore.sqf new file mode 100644 index 0000000..eae1d31 --- /dev/null +++ b/arma/server/addons/store/functions/fnc_initStoreStore.sqf @@ -0,0 +1,168 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initStoreStore.sqf + * Author: IDSolutions + * Date: 2026-03-12 + * Last Update: 2026-03-12 + * Public: No + * + * Description: + * Initializes the server-side store checkout flow. + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ + ["#type", "StoreBaseStore"], + ["#create", compileFinal { + ["INFO", "Store checkout service initialized!"] call EFUNC(common,log); + }], + ["buildResult", compileFinal { + params [["_message", "Checkout failed.", [""]], ["_paymentMethod", "cash", [""]]]; + + createHashMapFromArray [ + ["success", false], + ["message", _message], + ["paymentMethod", _paymentMethod], + ["chargedTotal", 0], + ["lockerGranted", []], + ["unsupportedLines", []], + ["bankPatch", createHashMap], + ["orgPatch", createHashMap], + ["orgTargetUids", []] + ] + }], + ["formatCurrency", compileFinal { + params [["_amount", 0, [0]]]; + + format ["$%1", [_amount max 0] call BIS_fnc_numberText] + }], + ["applyPaymentPatch", compileFinal { + params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_paymentMethod", "cash", [""]], ["_total", 0, [0]], ["_commit", false, [false]]]; + + 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", []] + ] + }; + }; + + if !(_payment getOrDefault ["success", false]) exitWith { + _result set ["message", _payment getOrDefault ["message", "Unable to process payment."]]; + _result + }; + + 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", []]]; + }; + + _result set ["success", true]; + _result set ["message", ""]; + _result + }], + ["checkout", compileFinal { + params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_payloadJson", "", [""]]]; + + private _result = _self call ["buildResult", ["Checkout failed.", "cash"]]; + private _payload = fromJSON _payloadJson; + private _paymentMethod = toLowerANSI (_payload getOrDefault ["paymentMethod", "cash"]); + private _totalPrice = floor ((_payload getOrDefault ["totalPrice", 0]) max 0); + private _items = _payload getOrDefault ["items", []]; + private _vehicles = _payload getOrDefault ["vehicles", []]; + + _result set ["paymentMethod", _paymentMethod]; + _result set ["chargedTotal", _totalPrice]; + + if (_items isEqualTo [] && { _vehicles isEqualTo [] }) exitWith { + _result set ["message", "Add at least one item before checkout."]; + _result + }; + + if (_vehicles isNotEqualTo []) exitWith { + _result set ["unsupportedLines", _vehicles apply { + createHashMapFromArray [ + ["classname", _x getOrDefault ["classname", ""]], + ["category", _x getOrDefault ["category", "vehicle"]], + ["reason", "Vehicles are handled through the virtual garage flow."] + ] + }]; + _result set ["message", "Vehicle purchases are not wired yet."]; + _result + }; + + if (_totalPrice <= 0) exitWith { + _result set ["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."]]; + _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."]]; + _result + }; + + _result set ["lockerGranted", _lockerPreview 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 _lockerPatch = _lockerResult getOrDefault ["patch", createHashMap]; + private _vaPatch = _vaResult 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); }; + + 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]; + 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 (_payment getOrDefault ["orgTargetUids", []]); + }; + + _result set ["success", true]; + _result set ["message", format [ + "Checkout completed. %1 charged, %2 locker grant(s).", + _self call ["formatCurrency", [_totalPrice]], + count (_lockerResult getOrDefault ["granted", []]) + ]]; + _result + }] +]; + +GVAR(StoreStore) = createHashMapObject [GVAR(StoreBaseStore)]; +GVAR(StoreStore) diff --git a/lib/repositories/src/org.rs b/lib/repositories/src/org.rs index 068c837..60340cd 100644 --- a/lib/repositories/src/org.rs +++ b/lib/repositories/src/org.rs @@ -127,6 +127,16 @@ impl OrgRepository for RedisOrgRepository { } // Reconstruct Org from JSON object + if matches!( + json_map.get("credit_lines"), + Some(serde_json::Value::Array(lines)) if lines.is_empty() + ) { + json_map.insert( + "credit_lines".to_string(), + serde_json::Value::Object(serde_json::Map::new()), + ); + } + let json_obj = serde_json::Value::Object(json_map); match serde_json::from_value::(json_obj) { Ok(org) => Ok(Some(org)), diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs index 76f8b60..05bddee 100644 --- a/lib/services/src/org.rs +++ b/lib/services/src/org.rs @@ -25,6 +25,31 @@ pub struct OrgService { } impl OrgService { + fn normalize_org_value( + mut org_value: serde_json::Value, + key_override: Option, + ) -> Result { + let org_object = org_value + .as_object_mut() + .ok_or_else(|| "Org payload must be a JSON object".to_string())?; + + if let Some(key) = key_override { + org_object.insert("id".to_string(), serde_json::Value::String(key)); + } + + if matches!( + org_object.get("credit_lines"), + Some(serde_json::Value::Array(lines)) if lines.is_empty() + ) { + org_object.insert( + "credit_lines".to_string(), + serde_json::Value::Object(serde_json::Map::new()), + ); + } + + serde_json::from_value::(org_value).map_err(|e| format!("Invalid Org JSON: {}", e)) + } + /// Creates a new organization service with the provided repository. /// /// The repository must be initialized and ready for use. @@ -37,12 +62,9 @@ impl OrgService { /// Handles validation, duplicate checking, and persistence. /// See [crate README](../README.md) for JSON format and business rules. pub fn create_org(&self, key: String, json_data: String) -> Result { - // Parse JSON data to Org struct - let mut org: Org = + let org_value: serde_json::Value = serde_json::from_str(&json_data).map_err(|e| format!("Invalid Org JSON: {}", e))?; - - // Override ID with the provided parameter (ensures consistency) - org.id = key; + let org = Self::normalize_org_value(org_value, Some(key))?; // Validate organization name is not empty if org.name.trim().is_empty() { @@ -60,21 +82,10 @@ impl OrgService { Ok(org) } - /// Retrieves an organization by its unique identifier with automatic fallback to default. - /// - /// Implements a fallback pattern: if the organization doesn't exist, the - /// organization with ID "default" is retrieved from the repository. pub fn get_org(&self, key: String) -> Result { - // Attempt to retrieve organization from repository - match self.repository.get_by_id(&key)? { - // Organization found - return it - Some(org) => Ok(org), - // Organization not found - retrieve the default organization instead - None => self - .repository - .get_by_id("default")? - .ok_or_else(|| "Default organization not found".to_string()), - } + self.repository + .get_by_id(&key)? + .ok_or_else(|| format!("Organization with ID '{}' not found", key)) } /// Updates an existing organization with new data from JSON. @@ -89,7 +100,7 @@ impl OrgService { }; // Parse and validate JSON update data - let update_data: serde_json::Value = + let mut update_data: serde_json::Value = serde_json::from_str(&json_update).map_err(|e| format!("Invalid JSON: {}", e))?; // Ensure update data is a JSON object @@ -97,6 +108,13 @@ impl OrgService { return Err("Update data must be a JSON object".to_string()); } + if matches!( + update_data.get("credit_lines"), + Some(serde_json::Value::Array(lines)) if lines.is_empty() + ) { + update_data["credit_lines"] = serde_json::Value::Object(serde_json::Map::new()); + } + // Create a temporary copy to safely apply updates with validation let mut updated_org = org.clone();